Expand description
The memory safety monitor: check insertion over the IR.
Design: spec/safe-memory/06-instrumentation.md section 6.3.
The one decision this crate exists to make is when checks are inserted. Every sanitizer that came before instruments after the optimizer, so that the optimizer cannot delete its checks, and pays the full naive cost of every one of them forever. We insert before the optimizer and let it discharge what it can prove, which is only possible because a check is an instruction with defined semantics rather than a call the optimizer has no opinion about.
§What is here so far
The three checks milestone S1 in spec/safe-memory/16-milestones.md asks for: bounds and
lifetime on every access, and a derivation check on every pointer computed from another
pointer. Nothing is discharged, so a function comes out with a check in front of everything,
which is the baseline every elimination claim at S4 is measured against.
And the boundary, in wrap: a call the program wrote to one of the C library functions
rucc-safe-rt has a row for is pointed at that row’s wrapper instead, so the judgements happen
before the call rather than not at all. That is milestone S2 and
spec/safe-memory/10-boundaries.md section 10.3 is what it implements.
And the other end of it, in lower: after the optimizer has run, every check still standing
becomes a call to the runtime carrying the index of a row in a table this crate puts in the
object. That module is where the reason S1’s checks are calls rather than compares is argued.
And the rest of the boundary, in boundary: the places where a pointer crosses between
this build and code nobody instrumented, which is a function of this file that somebody else can
call and a call this file makes to a library that has no wrapper. Neither can be modelled, so
each of them is counted instead, which is what section 10.2 says the honest answer to a question
you cannot answer is.
And what all of that came to, in summary: the counts --emit=safety-summary prints,
which are what spec/safe-memory/10-boundaries.md section 10.2 means by a trust set that is
counted per build rather than asserted.
And the first of the planes, in plane: every store records what the bytes it wrote were
stored through, which is the judgement C 6.5 says a store makes, and every copy carries whatever
the bytes it read said over to the bytes it wrote, which is the other half of the same rule.
Those two are every write the type plane has, and every read that names a type now asks the
plane whether the bytes agree with it, which is judgement J3. The order was deliberate: a check
against a plane that only some of the writes maintain reports on programs that are correct, so
the writes went in first and the question went in once they were all in.
And the second of them, over the same two writes: every store records that the bytes it wrote hold something, and every copy carries whether the bytes it read held anything over to the bytes it wrote. The init plane is one bit per byte, so a store records the same thing whatever it stored, and a copy is the reason padding a member by member fill never touched is still padding nothing wrote after the structure moves. Every read now asks whether anything ever wrote the bytes it is about to read, which is document 03’s Y6, and the writes went in first for the reason the type plane’s did.
And the one check that is not about a single access, in promise: a block that declares
restrict pointers keeps a record of what each of them reached, and every access through one of
them asks whether another got there first. That is judgement J8 and it is off unless the build
asks for it with -fsafety-restrict, which is the only check here that is, and the reason is on
rucc_session::Promise.
The padding rule of spec/safe-memory/09-type-init-and-races.md section 9.3 arrives here as one
number. A store carries how much padding the member it went through owns, and the range it
records is the wider of that and what it wrote, which is all of -fsafety-init=nopadding. How
far the padding goes takes a record’s layout and this crate reads IR, so the front end is what
decides it and MemInfo.owns is how the answer travels.
The two questions a read asks come apart in one place. A read the front end named no type for
asks the type plane nothing, because the question there is which type the bytes hold, and it
asks the init plane the same thing every other read does, because the question there is about
the bytes rather than about the access. What no load in any program asks about is padding: a
read compiled into a load reads a member and a member is never padding, so the reads that
cover padding are memcmp of two structures, hashing one and handing one to write, every one
of which is a call into the movement group of wrap. Which is why the flag selects what a
store records rather than what a read asks about: the reads that would need it are not loads.
The race check is not here, because the epoch plane is not written at all and a check against a
plane nobody maintains would either report on every access or on none. That is S6. Neither are
the other plane writes: meta_begin and meta_end for an automatic instance need the escape
analysis of document 08 section 8.4, and until that exists the only instances the runtime knows
about are the ones the allocator reports, which is also why a store to a local records into a
plane that is not there and costs a call that decides nothing.
§Why the rank matters
rucc-safety is rank 10, alongside rucc-lower and rucc-opt, so it can depend on neither.
That is the constraint and not an inconvenience: it consumes IR and produces IR, it never sees
the AST, and rucc-driver at rank 13 is what sequences it between the two.
spec/safe-memory/15-integration.md section 15.1 argues it out.
§Stability
Every crate in the workspace is published, and publishing implies a promise. This one is
tier 3: its Rust API is explicitly unstable and will change without a major version bump.
Depend on the rucc binary’s behaviour, not on this.
Re-exports§
pub use boundary::Sites;pub use boundary::WITNESS;pub use boundary::witness;pub use lower::Descriptor;pub use lower::SECTION;pub use lower::lower;pub use plane::Plane;pub use promise::Kept;pub use promise::promise;pub use summary::Frames;pub use summary::Summary;pub use summary::summarize;pub use wrap::INTERPOSED;pub use wrap::PREFIX;pub use wrap::redirect;
Modules§
- boundary
- Witnessing the pointers that cross between instrumented code and code that is not.
- lower
- Turning the checks that survived the optimizer into something the back end can generate.
- plane
- The vocabulary the type plane is written in, and the number each entry travels as.
- promise
- The
restrictcontract, which is judgement J8 and is a promise about a block. - summary
--emit=safety-summary: what this build’s guarantee actually rests on.- wrap
- Pointing a call at the C library at its wrapper instead.
Structs§
Enums§
- Promise
- Whether the
restrictcontract is checked, from-fsafety-restrict. - Subobject
- Whether an access has to stay inside the member it names, from
-fsafety-subobject.