Pre-RFC: `usize` semantics

An issue is using usize as shorthand for pointers in layout computations. For example, I've seen plenty of code which does size_of::<usize>() * 3 to compute the offset of the fourth field in something like #[repr(C)] struct(usize, *const u8, usize, u8). Currently we guarantee that this is valid.

Strict provenance APIs are already useful even without considering CHERI. I don't understand how using them to establish CHERI support makes things any worse for your targets. They are committing to less than what the existing ptr-to-usize casts are committing to.

Are you suggesting that the strict provenance functions should not use usize but some uaddr type? That is the only way I can see to support targets like yours. But then we are again in the realm of deeply invasive changes to the ecosystem. Or if some approach pops up where uaddr can be added in a future-compatible way then that approach can also be used to migrate the strict provenance APIs to uaddr later.

I don't think it is reasonable to expect the entire Rust ecosystem to make a distinction between uaddr and usize -- the cost is too high and the benefit too small. So given that, using usize in these APIs for now does not make it any harder to come up with a way to have these two types but only differentiate them on some targets.

3 Likes

Perhaps. in the alternative, usize needs to be compatible with ptraddr_t not size_t.

1 Like

I'm one of the other people working on Rust for Morello (CHERI). Just to address some of the concerns:

Thanks, we agree and we'll adjust the wording accordingly.

We can update the RFC to use the provenance model throughout if necessary. We originally wrote this RFC such that it isn't gated on strict provenance being adopted first. To that end, we have included some comment on as-cast-patterns that are still supported.

We haven't thought about non-mainstream architectures. What is the status quo size of Rust's usize and C's uintptr_t and size_t on those platforms? I take it you're concerned that you want to store things in usize which are bigger than the address size?

This would be a nice attribute, no objection from us, we can add it to the RFC if there's general support.

In our experiments we indeed never need to introduce a uintptr_t type. We think this is a significant win. We toyed with adding an alternative to this RFC which codified a Rust-uintptr_t integer type but couldn't find a good reason for it to exist and it would have expanded the scope of this RFC undesirably.

I think this subtle breakage will only ever rear its head when bringing code over to a CHERI platform, and will be benign (forever) on platforms like x86_64 and aarch64.

This is possible, we currently have a fork of Rust but it would be a pity for it to be a fork forever.

We think that a sensible bit of evidence to gather with this RFC would be to add a lint for pointer/integer casts that would be incorrect under this semantics and then do a crater run to measure the extent of the breakage. Anecdotally we've compiled quite a lot of Rust from crates.io to do some benchmarking, and very few crates were doing these casts. Where it was happening appeared to be exclusively around FFI shenanigans.


General comment: we have no expectation of immediate CHERI support in the Rust ecosystem, no one can buy CHERI hardware yet. It does appear that generally available CHERI hardware will appear in the medium future, IMO probably as CHERI RISC-V first.

3 Likes

In my view, the entire strategy here is based on strict provenance: CHERI support will work well in the ecosystem if we can get people to not do as casts, and use strict provenance APIs instead. as casts between integers and pointers should IMO have a warn-by-default lint on CHERI since they do not behave as expected. (IOW, CHERI should have warn(fuzzy_provenance_casts) by default, or maybe even forbid(fuzzy_provenance_casts).) I don't think we should tell people "replace your as casts by these particular magic functions full of even more as casts"; that would be a step backwards in code clarity. We should instead tell people "replace your as casts by strict provenance operations", which avoids the problematic int2ptr cast semantics, helps verification tools such as Miri, and helps CHERI -- win, win, win.

If strict provenance doesn't work out, then people having inlined versions of the strict provenance functions everywhere (which is basically what your examples are) seems bad.

So I very much think that this should be gated on adopting strict provenance.

11 Likes

The issue here is that the type that can store an address is the same (ish) size as a pointer, but size_t is smaller than both.

I do wonder how important the types used in the strict_provenance APIs are specifically - could they use a stdlib type alias non-intrusively? Otherwise, it may be acceptable if usize is defined to be compatible with ptraddr_t, which is either uintptr_t or size_t (platform dependent which).

3 Likes

I'm convinced. We'll update the RFC for the next round of comments and make it clear that it's gated by strict_provenance. Hopefully this also addresses the concern of @talchas where we're imagining this being introduced in a world where fuzzy_provenance_casts would get a warning anyway.

Just for clarification, I have been reading ptraddr_t as equal to size_t. I don't know of any C implementations where size_t isn't large enough to store an address, perhaps w65 is one.

Would a good solution for you be usize = ptraddr_t (so 16 bits on w65?) and ffi::uintptr_t be defined as u32 for w65?

It would be a much worse world if the semantics of usize was platform dependent between uintptr_t and ptraddr_t.

2 Likes

Here it's ptraddr_t = uintptr_t = u32 (an address and a pointer on w65 both includes the bank). size_t is 16-bit as the maximum object size is 32767 (in theory, it's 65535, but ptrdiff_t). This is done so that pointer offset calculations can be done using indexed addressing modes rather than a 32-bit add. It's the same story on 8086-far (though the layout of the top-16 bits of the pointer is far different), though there objects cannot cross a segment (crossing a segment is ill-defined, particularily since the generated code is intended to work with both 286 protected mode and 386+ 16-bit protected mode where the segment registers hold selectors rather than segment bases).

1 Like

I agree that this seems to be the worst side-effect of this change, but it will continue to de-facto work on existing architectures, just not on CHERI. Additionally, now that offset_of! exists, one would hope that the need for this kind of code should just go away.

6 Likes

I mean, it makes it a clear error if a crate is using expose_addr/etc, which is an improvement, yes. I'm still extremely negative on such experimental platforms (without even public hardware, without actual C support) having any expectation of support from general crates / making any constraint on them. And that's what you explicitly said you expected on zulip.

(Also: "We haven't thought about non-mainstream architectures" look CHERI is not even close to mainstream either; at least there's a number of people who still do SNES stuff, though rust for it is still kinda silly imo)

2 Likes

I don't know, I still think we've promised this layout is identical in extremely clear terms -- There's no ambiguity there. IMO this is not the kind of promise we should make and then see as flexible when it becomes inconvenient. I'm unconvinced by the arguments that this only impacts new targets, because most of this code will compile successfully, but misbehave on these targets.

My stance is that we probably will make CHERI-Rust a non-compliant implementation of Rust -- my understanding is that this will need to be the case anyway, due to the issues around wrapping_offset and similar APIs. Alternatively, making usize be 128 bits is likely preferable -- it has downsides, but it doesn't go back on any promises.

now that offset_of! exists, one would hope that the need for this kind of code should just go away.

Yes, I'm aware -- I was the author of the offset_of RFC. offset_of is still unstable, and may be for quite a while (sadly I would not expect it to stabilize by the end of the year, there are some thorny unresolved design questions about two incompatible but desirable extensions to its functionality, both of which may impact the overall syntax of the construct).

It also doesn't retroactively change old code to use it.

3 Likes

For most of the ecosystem it might as well not exist, until it is stable.

We should either support CHERI in rustc or we shouldn't; games like "oh, it isn't really compliant but..." aren't meaningful in that regard.

We can either decide we're never going to support it, or decide we're going to support it but via an opt-in for code caring about it (and without that opt-in code that wouldn't work on CHERI compiles without warning or error on other targets), or decide we're going to support it by default such that everyone has to care about it by default (so code that wouldn't work on CHERI gives a warning or error on other targets).

Speaking only for myself: I think the second of those options makes the most sense: let code opt into caring about targets where usize and size_t and pointers are not all the same size.

(Similarly, I also think we should make it opt-in to care about targets where u32 is bigger than usize.)

14 Likes

Thank you Sarah for working on a draft.

I do see a path forward towards eventually not only fully supporting architectures like CHERI, but also migrating the whole ecosystem to being portable to those architectures in a safe and incremental way.

For example:

  • Redefine usize to mean usize == memory address on all architectures; nothing changes for code targeting currently supported architectures on which "memory address size == pointer size".
  • Redefine the safety of ptr as usize and usize as ptr casts to safe on architectures in which "memory address size == pointer size" for current Rust editions. This does not require any changes on any pre-existing Rust code targeting any currently supported architecture on a current Edition. The safe as-cast version still compiles and runs. However, on CHERI, those casts are now unsafe, so programs containing those casts that want to run on CHERI would need to switch to writing unsafe { } and prove why that code is safe. The burden to do that falls on developers wanting to run code on CHERI, not on anyone else. This prevents some code - far from all - from accidentally exhibiting UB in safe Rust code when recompiled for CHERI.
  • Maybe provide an x86_64 / aarch64 target with 128-bit pointers, where 64-bit are used for the address, and 64-bit are initially unused, and maybe eventually used for software-tag-checking. This could help catch many of the cases where unsafe code may be assuming "address size == pointer size" today, e.g., for layout computations.
  • Once there is a sound replacement for integer-pointer casts in stable Rust, we can start warning on maybe some/maybe all integer-pointer as casts, recommending using the replacement.
  • On a subsequent Edition, we could switch to making those casts unsafe {} on all targets. That would only require crate authors to modify their code, if they wanted to switch their crates to that Edition. I haven't thought this last step through to its ultimate consequences , but it may exceed the syntactic change capabilities of Editions, e.g., while the change looks syntactic, it is not clear to me whether Rust soundness proofs would hold for code mixing these two editions, but arguably I am not sure they do hold for code that has integer/pointer casts today. So maybe this is ok, or maybe not, but either way, at least code that does not mix these two editions, is not worse of than the code we have today, and applications whose dependencies are all using the newer edition, would be significantly better of from both a soundness perspective, and a platform portability perspective.

There are probably many other paths to get there, and if we don't want to end there, there are many more paths that get us somewhere else.

I'm less interested in what all of these paths precisely look like and more interested in what is the smallest next incremental step we are confident enough to take to start walking, and ideally enable CHERI with no or minimum impact and risk to any of the pre-existing application and targets, ideally with a significantly higher weight of burden on those that want to work on targets like CHERI, than on those that don't care.

5 Likes

It does go back on promises, we also guarantee that usize == size_t. There is no way to support CHERI without going back on promises.

But my hope is that if we can nudge large pars of the ecosystem to honor fuzzy_provenance_casts, then that will implicitly also make those parts of the ecosystem CHERI-compatible. Code that uses usize to stand in for the size of a pointer for offset computation would still not work, of course. I'm not sure if the CHERI people need an official statement "such code is buggy" to have practical support though; I expect such code to be rare anyway and for the cases where it comes up, I think we can rely on the goodwill of the maintainers of the relevant crates (if those are relevant for CHERI) to accept appropriate patches.

There's not really a good justification for that as there's no UB from these casts. We don't use unsafe as a lint.

Making fuzzy_provenance_casts a future-compat warn/deny-by-default lint on such targets (so it is shown even for dependencies) is much better, IMO.

3 Likes

Would a good next step be to update this pre-RFC to switch to strict_provenance only and to add some conditions for fuzzy_provenance_casts warnings as you suggest, then?

Are there other concrete changes which would make this proposal more technically complete?

1 Like

Out of interest, where do we make that guarantee?

I don't believe it's guaranteed anywhere, but it's assumed by a lot of crates and de facto stable.

1 Like

Sure but that's very different to the actually documented promise listed in the OP.

4 Likes

I don't know, but it is as de-facto stable as uintptr_t == usize.

"how many bytes it takes to reference any location in memory" is completely undefined, so that's not really a guarantee that's useful for anything either.