Skip to main content

Crate rucc_safety

Crate rucc_safety 

Source
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 restrict contract, 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§

Counts
How many checks a run of insert put in.

Enums§

Promise
Whether the restrict contract is checked, from -fsafety-restrict.
Subobject
Whether an access has to stay inside the member it names, from -fsafety-subobject.

Functions§

insert
Puts checks in front of every access and every derivation in a function.
run
Puts checks in every function a module defines.