coercion of mutable pointers' child type causes a soundness issue
Zig Version
0.14.0-dev.367+a57479afc
Steps to Reproduce and Observed Behavior
Attempt to compile the following code:
pub fn main() void {
const n: u8 align(2) = 42;
const m: u8 align(1) = 0;
var t: *align(2) const u8 = &n;
var u: *align(1) const u8 = &m;
_ = .{ &t, &u };
const pt: **align(2) const u8 = &t;
const pu: **align(1) const u8 = pt; // this coercion is bad!
pu.* = u;
// oh no! we've stored an align(1) value in a
// location that expects to hold an align(2) value
}
The code compiles without errors.
Expected Behavior
The code should have triggered a compilation error.
A mutable pointer to (one / many / slice / C) a type T can coerce into a mutable pointer of type U without soundness problems if-and-only-if:
-
TandUare in-memory coercible, and - every valid bit-pattern of the only is a valid bit-pattern of the other.
These are, I believe, the most liberal coercion rules that do not result in soundness problems - I will leave the discussion about whether Zig should be this liberal with its coercion rules to another issue.
It's already noted in the issue text, but important to note that coercion of pointers-to-const *const *align(2) T -> *const *align(1) T can be allowed.
We only need to prevent it when the target pointer-to-mutable broadens the set of valid pointee values.
I just stumbled upon another manifestation of the issue, now with sentinel-terminated types:
test {
var x: [*:0]const u8 = "abc";
const good_ref: *[*:0]const u8 = &x;
const evil_ref: *[*]const u8 = good_ref; // this coercion is bad!
evil_ref.* = @as([*]const u8, &.{ 'a', 'b', 'c' });
// oops! `x` is now storing a
// non-sentinel-terminated pointer!
}
this is the classic unsoundness of covariance of mutable references. to lay out the sound conversions:
- a mutable reference is invariant on its child type
- a const reference is covariant on its child type
- a write-only reference (though such type does not exist in Zig...) is contravariant on its child type
in Zig we also care that the coercion is possible in-memory (i.e. we just reinterpret bits), and that mutable references to T can be cast into mutable references to U and back if T and U are in-memory coercible into each other
fixed by #22243