Soundness fix: respect read_scalar
errors in read_from_const_alloc
. #353
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
(If you've seen me mention how various work in this area, like #348, or even #350 or #341 which have already landed, is somewhat of a prerequisite, or at least arose during some bug fix, this is it, but I decided this is far too important to block on any other improvements, so this PR contains its most minimal form I can think of - the benefits from extra work is mostly diagnostics, but correctness comes first)
The method
read_from_const_alloc
(_at
) (suffixed after this PR) is responsible for reading the components of a SPIR-V constant of some typety
at anoffset
in some constantalloc
(i.e. a miri/CTFE memory allocation, so a mix of plain bytes, symbolic pointers, and uninitialized memory).The first problem was it using
&mut
foroffset
, and its mutation being relied on for both auto-advancing (e.g. between the components of a SPIR-V vector/matrix), but also for some ad-hoc checks.So the first commit in this PR refactors it to:
offset
(structs/arrays adding field/element sub-offsets to it)Size
for the constant value that was read, alongside that valuety
, this is guaranteed (and checked) to equal the size ofty
(i.e.
cx.lookup_type(ty).sizeof(cx) == Some(read_size)
)ty
, this mimics Rustmem::size_of_val
,(if
ty
is, or ends in[T]
, this will fit as manyT
elements as possible inalloc.size()
,after
offset
, so it'll almost always be the wholealloc
, minus at most a gap smaller thanT
)create_const_alloc
function (which used to check the final offset w/ anassert_eq!
) with anOption
-returningtry_read_from_const_alloc
that checks the readSize
againstalloc.size()
&CONST
because of pointer casts(e.g. if
ARRAY[i]
is equivalent to*ARRAY.as_ptr().add(i)
, and.as_ptr()
is a&[T; N] -> *T
cast,you really don't want that to become
*(const { &{ARRAY[0]} } as *const T).add(i)
and UB fori > 0
)read_from_const_alloc
inconst_bitcast
(the main way&CONST
gets a type) alreadyfits the conditional nature of
try_read_from_const_alloc
(and other refactors break w/o such a check)&CONST
use ofcreate_const_alloc
was for the initializer ofstatic
s, and that can alwaysunwrap
thetry_read_from_const_alloc
(initializeralloc
is always the size of thestatic
's type)Most of that refactor isn't, strictly speaking, necessary right now (other than making the code less fragile/error-prone), but it's a much cleaner solution than all the workarounds I had previously come up with, downstream of the soundness fix (and e.g. #348 + calling
const_bitcast
frompointercast
in more cases).The big soundness issue, however, was that
read_from_const_alloc
, for primitive (int/float/pointer) leaves, would callalloc.read_scalar(offset, ...)
, and treatErr(_)
as "undef
value at that location".But a whole
undef
value is a very specific case, while the returnedAllocError
can indicate:undef
(i.e.
alloc
has a pointer that starts just before/afteroffset
, but not atoffset
exactly)Unsoundness arises from spurious
undef
(OpUndef
in SPIR-V) being misused instead of reporting an error, because it's designed to be ignored by optimizations (or even routine transformations like control-flow structurization), and treated like it can take on any value (i.e. it makes it UB to care about the exact value).Even worse, Rust-GPU is prone to attempt to represent constant data as e.g.
[u32; N]
, and if thealloc
contains any pointers, reading them asu32
will result inErr(AllocError::ReadPointerAsInt(_))
, and before this PR the pointers would silently be ignored and turned into uninitialized memory.So the second commit in this PR actually handles the
AllocError
, and only uses a plainundef
when all bytes are uninitialized, all other cases being errors - with the caveat that doing more work to produce the correct constant may be possible in some cases, but I haven't put too much effort into it.For now, the one special-case is that it does try to turn "whole pointer attempted to be read as an
usize
" errors into ptr->intconst_bitcast
s (of the actual pointers) instead, which doesn't do much in terms of debuggability, just yet, but future work to improveconst_bitcast
does help here.In theory,
OpSpecConstantOp
would let us represent e.g. only some bits beingOpUndef
/some pointer, by mixing constants using bitwise ops (e.g.(undef << 24) | ((ptr as u32) >> 8)
), but it's more likely we'll first get more untyped constant data, than ever need this.