Five Ways a Safety Contract Can Break

CULPA’s memory-safety model starts with a practical question: when an unsafe operation is allowed, what must be true about the pointer, the memory, and the value being produced? This explanation uses the five requirement categories in the CULPA study to give each answer a distinct meaning. By the end, a reader should be able to classify a failure by the missing condition rather than treating every unsafe operation as one undifferentiated kind of danger.

A memory-safety bug is a missing condition

A raw pointer is an address, but an address alone says very little about whether a read or write is valid. The pointer may refer to the wrong allocation, point to a location that has already been freed, or identify bytes that are not a legal value of the type the program intends to use. The length used with the pointer may also describe a larger range than the allocation actually contains. CULPA turns these possibilities into five categories of safety requirement.

InBound asks whether the operation stays within the intended memory boundary. The relevant boundary is not always the end of an entire allocation. It might be the initialized part of a container, the field of a structure, or a range derived from a pointer and a length. An unchecked element access can therefore be wrong even when the pointer itself is non-null and correctly aligned: the index may simply reach beyond the region that belongs to the operation.

NonNull asks whether a pointer has a valid non-null value. A null pointer is a particular failure, not a synonym for every invalid pointer. A program might perform an explicit null check, receive a non-null result from a trusted constructor, or accidentally turn a previously valid pointer into a null one. The evidence must concern the pointer that reaches the unsafe operation, not merely an earlier pointer with a similar role.

Alignment asks whether the address is suitable for the type being read or written. Types can require their values to begin at particular byte boundaries. An address can lie inside an allocation and still be unsuitable for interpreting those bytes as the requested type. This is why a bounds check cannot discharge an alignment obligation. It answers where the access ends, not whether the starting address has the right layout.

Initialization asks whether the memory contains initialized, type-valid values. “There are bytes at this address” is weaker than “there is a valid value of this type at this address.” Some types permit many bit patterns; others impose stricter rules. A region can have been allocated but never written, or it can have been written with a bit pattern that the destination type does not permit. Both cases fail the evidence needed for a typed read, although the underlying mistake is not necessarily the same.

ExclusiveAlias asks whether the access respects the required relationship among pointers and references over time. A pointer can be non-null, in bounds, aligned, and aimed at initialized data while still violating the rules about simultaneous mutable access or access after deallocation. This category is the one CULPA uses to reason about temporal failures such as use-after-free. The paper’s advisory corpus did not contain a case classified under this category, but the detector includes it because the obligation is part of the memory-safety model.

The categories describe conditions, not a strict menu in which every operation chooses exactly one. A raw-pointer dereference may need all five. A particular unsafe API may emphasize a subset. The useful discipline is to ask which condition the API documentation requires and which evidence in the calling path supports that condition. A null check does not prove initialization. An aligned allocation does not prove a legal value. A valid length does not prove that the bytes in the range have ever been written.

This separation also prevents a misleading conclusion about CULPA’s taxonomy. The five categories cover the memory-safety scope selected by the study; they are not a complete classification of every undefined behavior or every reason Rust code can be incorrect. They give the detector a common vocabulary for several related failures while leaving other problems—such as logic errors, ordinary panics, many foreign-function issues, and multithreaded interactions—outside its central model.

The categories become useful when they are treated as questions about a particular state, not as permanent labels attached to a line of code. Suppose a function receives a raw pointer and a count. The reviewer must ask whether the pointer is non-null, whether the complete counted range belongs to the intended allocation, whether the starting address is aligned for the type, whether every value in that range is initialized and legal, and whether another access can invalidate the assumption before the read finishes. A caller can satisfy one question while failing another. A non-null pointer can name freed memory; a valid allocation can be too short for the count; an aligned range can contain an invalid typed value. The proof is therefore a conjunction of facts about one object at one point in its lifetime.

Those facts also live at different levels. NonNull and Alignment concern the pointer’s relationship to the address space and the destination type. InBound concerns the relationship between an operation’s range and an object’s boundary. Initialization concerns the contents and the legal values they represent. ExclusiveAlias concerns the history of accesses and the time over which a pointer remains usable. CULPA can analyze them through one contract-oriented pipeline because they all ask whether a required safeguard reaches an unsafe operation, but the evidence for each safeguard is different. Keeping the levels distinct is what prevents a nearby check from being credited with proving the wrong property.

The same unsafe permission can hide different failures

The distinction becomes clearer when the same broad operation is examined through different facts about its input. The paper’s examples are valuable because they do not present “unsafe code” as one pattern. They show how the object, the portion of memory, and the type interpretation determine which proof is missing.

An unsafe type conversion is the first case. Reinterpreting a value from one type as another can require the address to meet the destination type’s alignment. A small source type may be placed at an address that is valid for the source but not for a larger destination type. The conversion can therefore fail the Alignment requirement even when the original object was valid. But alignment is only the beginning. If arbitrary bytes are reinterpreted as a type with restricted bit patterns, the resulting value may be invalid even when the address is perfectly aligned. In the paper’s example, converting an arbitrary byte into bool can create an illegal value because bool does not treat every byte pattern as a valid Boolean.

This is why the phrase “valid at its given type” is too compressed to serve as one check. It hides at least allocation validity, alignment, initialization, and the set of values the destination type permits. A detector that checks only the address boundary can miss an invalid value. A detector that checks only the type conversion pattern can miss a pointer that begins at the wrong offset. The operation needs several independent pieces of evidence.

The Vec::set_len example exposes a different combination. Vec::with_capacity reserves room for a vector, but its spare capacity is not automatically initialized as elements of the vector’s type. When set_len changes the length, it tells the vector that a larger prefix now contains elements. The caller must establish that the new length is no greater than the capacity. The caller must also establish that every element in the newly exposed range has been initialized with a legal value. If the length exceeds capacity, a later access can cross the allocation boundary. If the length is within capacity but the new range was never initialized, later safe-looking reads can expose invalid memory contents. One unsafe operation therefore carries both an InBound and an Initialization obligation.

The example also shows why the word “length” is not enough to identify a boundary. A vector has at least two relevant regions: the initialized prefix and the remaining allocated capacity. Increasing the length changes the program’s interpretation of the second region. The proof must follow that state change, not simply record that a vector allocation once succeeded.

A third case separates alignment from value validity in the other direction. The paper describes a MemoryRange constructor that establishes an aligned memory layout before a later call to slice::from_raw_parts, which interprets the range as a slice. That constructor can provide strong evidence for the Alignment requirement. But the later method accepts an arbitrary element type. Choosing a type such as bool can still create an illegal value from the bytes in the range. A constructor’s guarantee is real, but it is specific: alignment does not silently expand into initialization or type validity. The constructor and the slice conversion are connected, but they do not have identical contracts.

The UTF-8 example demonstrates how a guarantee can be lost after it was established. A &str value carries a promise that its bytes are valid UTF-8. A wrapper can preserve that promise while iterating over characters, then break it by converting characters into single bytes. Multibyte characters are truncated in that transformation. If the wrapper subsequently calls an unchecked UTF-8 conversion, the unsafe operation is reached with evidence that no longer holds. The problem is not that the original string type was inherently unsafe. The problem is that a later operation changed the bytes without preserving the invariant that made the type safe to use.

Read side by side, the examples form four different evidence histories. In the conversion case, the destination type demands properties that the source type never promised. In the vector case, allocation creates capacity but not initialized elements, so writes must occur before length is increased. In the MemoryRange case, construction supplies alignment, but a later generic interpretation adds a separate value-validity question. In the UTF-8 case, a safe invariant exists at the start and is revoked by a transformation. The final unsafe calls may all look like short operations, yet the proof they need begins at different points and changes for different reasons.

This is also why a contract should identify more than an operation name. It should make clear which argument or object owns the relevant state, which portion of that object is in scope, which type determines legal contents, and which transitions can revoke the guarantee. The same numerical length can be safe for one allocation and too large for another. The same address can be valid for one type and misaligned for another. The same pointer can be usable before deallocation and invalid afterward. Without those relationships, a detector can report that a condition exists while losing the fact that gives the condition meaning.

These cases give provenance a concrete meaning. A safeguard must be attached to the same value or memory region that reaches the sink. It must also survive the transformations between the point where it was established and the point where it is consumed. A check on one pointer does not protect an alias that was offset into another region. A constructor’s alignment guarantee does not validate arbitrary contents. A safe type’s invariant cannot be treated as permanent if unsafe code has changed the underlying bytes.

flowchart TD U[Unsafe operation] --> B[InBound: intended range] U --> N[NonNull: usable pointer] U --> A[Alignment: suitable address] U --> I[Initialization: legal value] U --> X[ExclusiveAlias: valid access over time] B --> O[Out-of-bounds access] N --> P[Null-pointer dereference] A --> M[Misaligned access] I --> V[Uninitialized or invalid-value access] X --> F[Use-after-free]

The graph is a teaching aid for a causal distinction: the same unsafe operation can depend on several conditions, and a failure at one point can produce a different memory-safety outcome from a failure at another. The categories can overlap in a real bug, but they should not be merged merely because the final symptom is “memory corruption.” Preserving the distinction helps a reviewer ask for the right evidence and helps an analyzer reuse the same contract machinery across different manifestations.

It also explains CULPA’s central research strategy. The authors analyzed 64 memory-safety-related bugs from 39 RustSec advisories and report that all of those selected cases violated safety requirements documented for unsafe APIs. That observation supports looking for the broken requirement beneath the visible symptom. It does not show that the five categories cover every unsafe-Rust defect, nor does it turn the advisory sample into a prevalence estimate. What it does show is why a detector can be organized around obligations rather than around an ever-growing list of bug patterns.

The next difficulty follows directly from this model. Naming the condition is not enough if the analyzer cannot tell which part of an allocation the condition governs, where the supporting evidence came from, or whether later code invalidated it. CULPA’s next layer addresses that problem by translating these categories into executable predicates and carrying their evidence across the operations that change memory’s meaning.

References