Pre-RFC: Rust Safety Standard

Fair enough; though "x must be properly aligned for u32" doesn't technically mean anything I think, since x is a value, not a pointer. (And if it were a pointer (or reference) it could be properly aligned for u32 without that being a requirement of the type.)

Saying "T must have alignment greater than or equal to that of u32" would be more precise (though as @ais523 points out, this is technically insufficient since u32 is allowed to have alignment less than its size), especially since saying x for the first requirement and T for the second when you actually mean a property of the type in both cases is inconsistent.

(If x were &T then saying "x must be properly aligned for u32" would be meaningful but then the second condition would need to be modified as said in my original comment.)

By the way, I have a conceptual picture in mind for handling unsafe code.

  1. This standard is a concrete first step that we believe the community can realistically take.
  2. The second step, in our view, is to introduce keyword-based safety properties, as suggested by Predrag here.
  3. The third step is to adopt attribute-based safety keywords (safety-tags), as proposed in RFC 3842. We proposed RFC 3842 half a year ago, but we found that it would be difficult to adopt in a real-world project unless the first two steps are implemented.
  4. The final step is to support parameters in safety tags, making them more expressive and enabling their conversion into contracts suitable for verification purposes. This is the goal of our safety-tag project.

M68k has 2 byte alignment for u32. I don't remember if there is upstream Rust support yet, but I believe retro enthusiasts are working on it, possibly using codegen_gcc. There are also some other even more obscure retro platforms with similar things iirc.

I have updated the document according to your suggestion. Although 4-byte alignment implies size = 4 * n, they are different properties. Thanks.

This is now a tautology. A function is safe unless declared unsafe, by definition. This is regardless of whether it uses unsafe code internally.

Perhaps you meant to say "can be marked safe", but if so, this would just be wrong for reasons stated previously. All functions in a module work together, unsafe code in a different function of the same module may be just as relevant as unsafe code in the same function.

This is incorrect. A free function can access a struct's fields by design. It is neither disallowed nor discouraged.

If the field is private, all functions in the module can access it. You might be thinking of the C++ or Java privacy models. Rust has a different model. In Rust, privacy boundaries are defined by modules, not by types.

This is true, of course. What I want to say is that we treat all direct field accesses as invoking the struct’s implicit (literal) methods.

I have rephrased the sentence as follows:

In particular, we conceptually treat struct construction and field access as follows: a free function creates a struct instance only via the struct’s constructors (including struct literals), and any direct field access is modeled as an invocation of the struct’s implicit methods.

This is now fixed as follows:

  • Function Safety Rule 1: If a free function contains unsafe code, it can be declared safe only if all conditions required for the safe use of that unsafe code are met.
  • Function Safety Rule 2: If a free function contains no unsafe code, there is no mandatory contract to be upheld, and it can be declared safe.

Note that Function Safety Rule 2 continues to hold even when the function calls other functions that contain unsafe code, provided that those callees themselves satisfy Function Safety Rule 1 and do not impose additional safety contracts.

You're going in circles. This is just false. There might be a requirement on the caller for reasons other than internal unsafe code. new_even_unchecked has no unsafe code in its implementation but it can not be declared safe.

3 Likes

This is currently incorrect.

The hope is that https://github.com/rust-lang/rfcs/pull/3458 will allow people to choose to move to a world where that's true for their unsafe code, but currently it's straight-up wrong, as Vec::set_len shows.

This topic is discussed in Section 3 (originally Section 6), which has been moved forward to establish a common stance before introducing the rules.

  • Weak Struct-level Soundness Criterion: This criterion ignores the risk that a type instance could be created or modified via a struct literal that violates the struct’s invariant.

This criterion is the one most commonly employed by the Rust standard library. For example, Vecis defined as follows:

pub struct Vec<T, #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global> {
    buf: RawVec<T, A>,
    len: usize,
}

Developers within the same module can easily create a Vec instance or modify len via struct literals in ways that violate the type invariant. However, this risk may be mitigated in the future through the use of unsafe fields.

Can you suggest better actionable rules? From my perspective, the difficulty comes from considering too many concerns at once—particularly literal constructors, as discussed in Section 2.3 (Decoupling), and visibility.

To establish a common baseline, I have added a new Section 3: Visibility-based Soundness Criteria (its also there in the original version as Section 6) before discussing the rules for free functions.

Note that decoupling the safety responsibilities of program components is essential for providing actionable guidance to developers.

Moreover, while one may construct counterexamples to a given rule in the standard, this does not necessarily imply that the example represents a good design: such designs may unnecessarily interleave the safety responsibilities of multiple program components.

@scottmcm Thanks for participating in the discussion. Do you have any suggestions or examples we could consider?

I spent an hour to persuade @hxuhack about it in a meetup, and he remains unconvinced. :joy:

1 Like

Have you considered the fact that maybe this point is controversial and this criterion should not be ignored?

Module-level safety invariants play a very important role because otherwise a ton of unsafe usages would not be considerable safe. Pretending they don't need to be upheld while still assuming the invariants hold when checking unsafe code just doesn't make sense.

I would personally suggest you to focus on the following areas:

  • don't distinguish free functions and methods since the only differences are syntactic
  • better integrate visibility/module level invariants since they play a fundamental role in most unsafe codebases
5 Likes

What is the problem with those rules?

  1. A function (in the public API of a crate) should be marked unsafe if safe code can cause UB by calling it.
  2. A trait (in the public API of a crate, and not sealed) should be marked unsafe if safe code can cause UB by implementing it.
  3. A field (in the public API of a crate) should be marked unsafe if safe code can cause UB by reading or writing it.
  4. A crate should be marked unsafe if safe code can cause UB by linking it (this concept doesn't exist yet but will eventually be needed).

Rules 1, 2, and 3 become recommendations when removing the stuff between parentheses.

Note that those rules only tell you when to mark something unsafe and not when to not mark them unsafe (or positively, when to keep them safe). It is always fine to mark something unsafe (it is then a usability concern).

1 Like

Thanks for the suggestion. From what I can tell, the problem is that these rules do not take visibility (e.g., module or crate) into account, and therefore the soundness criteria are unclear with respect to verification. More importantly, the rules are too coarse-grained: developers may incorrectly declare an API as safe, especially as a struct or module grows large and complex.

It is therefore beneficial to introduce fine-grained guidance that helps developers reason about safety responsibilities more systematically, as suggested in this document. Such guidance may not be the only valid approach to code design, but it should be considered a good practice.

The rules have crate-level visibility (the stuff between parentheses). For the recommendations, the user may choose where to draw the line. It's a matter of taste. Some people mark functions as unsafe even if they are private with a single call-site, claiming that it simplifies unsafe review.

I actually believe this to be a good thing. Nobody is going to read and remember fine-grained rules. And if the goal is to automate those rules (so humans don't have to read them), then you'll need a concept of "the scope of unsafe ends here". Because the tool can't tell if unsafe code is contained within some safe API (that's undecidable), so you need a way to disable the alert manually.

1 Like

To me it's usually pretty clear: it should match visibility. For example, if a struct's fields are private to the module, then any potentially invariant-violating functions on those structs exposed by the module have to be unsafe in order to maintain encapsulation within the module.

1 Like

I agree that this is cleaner and also provides nice properties like being able to factor a module out of a crate without much code change and thus risks (ignoring issues like orphan rule).

I might be too cautious, but my reasoning to only make this stricter version a recommendation, is to make a rule only what's strictly necessary for a healthy ecosystem (where interfaces are crates). The stricter version only matters at the project level, and each project may have its own opinion and style (without impacting the regulated ecosystem). The ecosystem would still recommend projects to follow the stricter version because it's cleaner.

(Note that if there were a notion of private crates, my rule would only apply to public API of public crates.)

@tczajka Do you think module-level soundness should be the default goal, given that a module is the smallest visibility unit enforced by the Rust compiler?

@ia0 Are you suggesting that crate-level soundness is the most relaxed yet still reasonable criterion that the ecosystem would recommend?

I have listed these as two soundness criteria that a project may adopt flexibly, but the project owner should be explicit about which criterion is being adopted, as it guides the subsequent rules for unsafe code checking. In addition, there are options related to struct-level soundness, which are stronger. Do you think it is worth mentioning these as well?