-
Notifications
You must be signed in to change notification settings - Fork 78
Soundness fix: respect read_scalar errors in read_from_const_alloc.
#353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| let value = if offset.bytes() == 0 { | ||
| base_addr | ||
| } else { | ||
| self.tcx | ||
| .dcx() | ||
| .fatal("Non-zero scalar_to_backend ptr.offset not supported") | ||
| // let offset = self.constant_bit64(ptr.offset.bytes()); | ||
| // self.gep(base_addr, once(offset)) | ||
| }; | ||
| if let Primitive::Pointer(_) = layout.primitive() { | ||
| assert_ty_eq!(self, value.ty, ty); | ||
| value | ||
| } else { | ||
| self.tcx | ||
| .dcx() | ||
| .fatal("Non-pointer-typed scalar_to_backend Scalar::Ptr not supported"); | ||
| // unsafe { llvm::LLVMConstPtrToInt(llval, llty) } | ||
| } | ||
| self.const_bitcast(self.const_ptr_byte_offset(base_addr, offset), ty) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This drive-by change is the minimal subset of backporting this (not yet landed) upstream PR:
I already have later changes to this code that bring it even closer to that version, but I only did this part for the special-case of "reading a Scalar::Ptr as an integer", because the old assert_ty_eq! would fail (while compiling libcore, IIRC?) even though the whole point is to end in a const_bitcast regardless of what ty is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More straightforward than the other ones! I agree, a result would be better, but I wouldn't block landing this on it.
| /// returning that constant if its size covers the entirety of `alloc`. | ||
| // | ||
| // FIXME(eddyb) should this use something like `Result<_, PartialRead>`? | ||
| pub fn try_read_from_const_alloc( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this would be better with something like Result<(SpirvValue, Size), ReadConstError>
enum ReadConstError {
PartialRead {
read: Size,
expected: Size,
},
Unsupported(&str),
InvalidLayout(String),
Zombie(String),
}and having the other fns return it too, giving something like:
match read_from_const_alloc_at(...) {
Ok((val, size)) if size == alloc.size() => Ok(val),
Ok((_, size)) => Err(ReadConstError::PartialRead {
read: size,
expected: alloc.size(),
}),
Err(e) => Err(e),
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's good reasons for the way this works, for the two kinds of "errors":
- individual errors
read_from_const_alloc_atturns into "zombies"- these are like Rust diagnostics (but deferred in the "zombie" way)
- eventually all of these will go away anyway (worst case showing up as
qptrdiagnostics) Resultis an anti-pattern for diagnostics, because it stops at the first error
(which can be fine for leaves, but anything recursive runs into "error buffering" needs)
Nonepotentially being returned bytry_read_from_const_alloc- this isn't even an error, maybe I should've named it
try_read_whole_const_alloc - the size check prevents the opportunistic replacement of
&CONST_Aw/&CONST_B
(ifCONST_Bis effectively some prefix ofCONST_A, i.e. a truncation) - if we wanted to e.g. make
const_fold_loadmore flexible, this wouldn't be used
(instead,read_from_const_alloc_atwould be called directly and always succeed)
- this isn't even an error, maybe I should've named it
(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 typetyat anoffsetin 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
&mutforoffset, 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)Sizefor 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
tyis, or ends in[T], this will fit as manyTelements as possible inalloc.size(),after
offset, so it'll almost always be the wholealloc, minus at most a gap smaller thanT)create_const_allocfunction (which used to check the final offset w/ anassert_eq!) with anOption-returningtry_read_from_const_allocthat checks the readSizeagainstalloc.size()&CONSTbecause of pointer casts(e.g. if
ARRAY[i]is equivalent to*ARRAY.as_ptr().add(i), and.as_ptr()is a&[T; N] -> *Tcast,you really don't want that to become
*(const { &{ARRAY[0]} } as *const T).add(i)and UB fori > 0)read_from_const_allocinconst_bitcast(the main way&CONSTgets a type) alreadyfits the conditional nature of
try_read_from_const_alloc(and other refactors break w/o such a check)&CONSTuse ofcreate_const_allocwas for the initializer ofstatics, and that can alwaysunwrapthetry_read_from_const_alloc(initializerallocis 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_bitcastfrompointercastin 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 "undefvalue at that location".But a whole
undefvalue is a very specific case, while the returnedAllocErrorcan indicate:undef(i.e.
allochas a pointer that starts just before/afteroffset, but not atoffsetexactly)Unsoundness arises from spurious
undef(OpUndefin 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 thealloccontains any pointers, reading them asu32will 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 plainundefwhen 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_bitcasts (of the actual pointers) instead, which doesn't do much in terms of debuggability, just yet, but future work to improveconst_bitcastdoes help here.In theory,
OpSpecConstantOpwould 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.