-
Notifications
You must be signed in to change notification settings - Fork 385
Get rid of the integer allocation #197
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
src/eval_context.rs
Outdated
@@ -872,7 +871,8 @@ impl<'a, 'tcx> EvalContext<'a, 'tcx> { | |||
let pointee_size = self.type_size(pointee_ty)?.expect("cannot offset a pointer to an unsized type") as i64; | |||
return if let Some(offset) = offset.checked_mul(pointee_size) { | |||
let ptr = ptr.signed_offset(offset, self.memory.layout)?; | |||
self.memory.check_bounds(ptr, false)?; | |||
// Why are we doing a bounds check on a pointer? |
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.
cc @RalfJung
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.
Pretty sure this is the offset
method which is UB on out-of-bounds.
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.
So... the offset
intrinsic is illegal on integral pointers?
And the same goes for the Offset
MIR-operator?
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.
I think so. The standard library uses arith_offset
when using integer pointers.
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.
These are the semantics that miri follows, so we're all good.
ce810a8
to
d53d154
Compare
A few bugs snuck in, but they're all fixed now |
src/memory.rs
Outdated
} | ||
|
||
pub type TlsKey = usize; | ||
|
||
#[derive(Copy, Clone, Debug)] | ||
pub struct TlsEntry<'tcx> { | ||
data: Pointer, // Will eventually become a map from thread IDs to pointers, if we ever support more than one thread. | ||
data: PrimVal, // Will eventually become a map from thread IDs to pointers, if we ever support more than one thread. |
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.
The comment needs updating.
src/memory.rs
Outdated
return Some((dtor, old_data)); | ||
pub(crate) fn fetch_tls_dtor(&mut self) -> Option<(ty::Instance<'tcx>, PrimVal)> { | ||
for (_, &mut TlsEntry { ref mut data, ref mut dtor }) in self.thread_local.iter_mut() { | ||
if let Some(dtor) = dtor.take() { |
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 changes the behavior, right? Now the dtor becomes None
. If the dtor initializes this variable back to something non-NULL, this will mean the dtor is not run again. I think that's wrong, pthread says the dtors are run multiple times until the variables are all NULL.
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.
hmm... The previous code also set data
to null. Can the destructor set it back to a value?
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.
Yes it can. The pthreads doc explicitly says that implementations have to check data again after the dtor runs, and repeat until data is NULL for all thread-local variables. (There's also some limit to prevent endless destructing. miri currently doesn't implement that limit.)
The docs are actually pretty good, as they fairly specifically they how to implement this:
An optional destructor function may be associated with each key value. At thread exit, if a key value has a non-NULL destructor pointer, and the thread has a non-NULL value associated with that key, the value of the key is set to NULL, and then the function pointed to is called with the previously associated value as its sole argument. The order of destructor calls is unspecified if more than one destructor exists for a thread when it exits.
If, after all the destructors have been called for all non-NULL values with associated destructors, there are still some non-NULL values with associated destructors, then the process is repeated. If, after at least {PTHREAD_DESTRUCTOR_ITERATIONS} iterations of destructor calls for outstanding non-NULL values, there are still some non-NULL values with associated destructors, implementations may stop calling destructors, or they may continue calling destructors until no non-NULL values with associated destructors exist, even though this might result in an infinite loop.
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.
Seems like infinite looping is valid behaviour. I vote we go with infinite looping, since miri already has an instruction counter
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 that's what I implemented -- or at least, it's what I intended to implement.
EDIT: Probably it'd make sense to copy the paragraphs above into the source.
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.
I think we have to do the latter, because executing one dtor could affect the data value of another TLS key.
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.
That's how I read it, too. Now it gets funky: what if a destructor creates a new key? We'll have a classical "collection modified while iterating" situation.
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.
I would think it gets added to the first round, but could be convinced otherwise.^^
I imagine something like the following (horribly inefficient) pseudocode:
while there_is_any_key_with_non_null_dtor_and_data() {
let mut i = 0;
while i < keys.len() {
keys[i].run_dtor_if_non_null_data();
i += 1;
}
}
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.
Fun fact: the previous impl was with a HashMap
, which a motivated developer could have used to write a nondeterministic random number generator in miri.
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.
Ah, right, HashMap
per default actually sources from the OS PRNG... nice catch. :D
} | ||
val | ||
} | ||
let (left, right) = (normalize(left), normalize(right)); |
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.
Yay for this being gone :)
src/value.rs
Outdated
pub fn wrapping_signed_offset(self, i: i64, layout: &TargetDataLayout) -> EvalResult<'tcx, Self> { | ||
match self { | ||
PrimVal::Bytes(b) if b as u64 as u128 == b => Ok(PrimVal::Bytes(wrapping_signed_offset(b as u64, i, layout) as u128)), | ||
PrimVal::Bytes(_) => Err(EvalError::OverflowingMath), |
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 is explicitly intended to wrap, how can it ever make sense to complain about overflow here?
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 should be an unreachable probably. I don't see how you can get a u128
to go through this path. That's the only overflow that can happen...
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 it seems like the types should prevent that. unreachable
or bug
both seem appropriate.
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.
Btw, does the same apply to the non-wrapping offset?
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.
I can't think of a way to reach it. So let's just make it unreachable and fix it if it becomes a problem.
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.
I think the condition in the first branch should be assert_eq!(b as u64 as u128, b)
because it's a misuse of this method on a PrimVal::Bytes
that is not a pointer.
@@ -33,21 +32,21 @@ impl<'a, 'tcx> EvalContext<'a, 'tcx> { | |||
self.memory.clear_packed(); | |||
self.inc_step_counter_and_check_limit(1)?; | |||
if self.stack.is_empty() { |
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.
Instead of triggering this on "stack empty" (and duplicating code), what about making the main function's cleanup StackPopCleanup::Tls(0)
(or StackPopCleanup::Tls(None)
)?
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.
Good idea
Some(key) => Excluded(key), | ||
None => Unbounded, | ||
}; | ||
for (&key, &mut TlsEntry { ref mut data, dtor }) in self.thread_local.range_mut((start, Unbounded)) { |
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.
Where does the "start all over again when we reached the end of the table" happen?
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.
When the stack is empty, just like before
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.
Ah, I see. That also explains why you kept the "stack cleanup" to be None
for the initial main.
I've run this PR on the rustc test suite, and we get a bunch of failures, because sometimes integer pointers are used for zst pointers, which used to be fine, since the pointers were passed on until they reached the The alternative is to sprinkle around zst checks everywhere, which gets tedious fast and is quite error prone |
@@ -893,6 +893,10 @@ impl<'a, 'tcx> EvalContext<'a, 'tcx> { | |||
} | |||
// FIXME: assuming here that type size is < i64::max_value() | |||
let pointee_size = self.type_size(pointee_ty)?.expect("cannot offset a pointer to an unsized type") as i64; | |||
if pointee_size == 0 { | |||
// rustc relies on offsetting pointers to zsts to be a nop |
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.
Where does this occur? So far I've only observed "offset-by-0" on ZSTs. If the offset is >0, this sounds very fishy from an LLVM perspective.
Is there even anything left that takes the
Alignment should usually still be checked on integer pointers though, shouldn't it? rustc went from using "1 as *const _" to using "align as *const _" to ensure that things are properly aligned. |
Some(it.into_slice()) | ||
} | ||
|
||
pub fn main() { |
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.
I think it was this test @RalfJung that triggered offset for zsts
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.
All right I will look into this. This is interesting from an unsafe code guidelines standpoint.
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.
Indeed, something fishy may be going on, but it's not miri's fault. ;)
See rust-lang/rust-memory-model#38 (comment).
There are a few, but they should often not exist and rather require changing our api |
Sounds reasonable. |
Hmm I think we should stop testing without full mir |
Ideally we could mark tests to be ignored without full MIR -- feel that makes some of the debugging simpler if one can start focusing on small things, without having to support the entire libstd boilerplate. (I am currently using that in the "detect UB"-checks I am developing.) Maybe have two folders, "run-pass" and "run-pass-simple" or so? |
whoops... reopened as #200 |
impl ParallelDrainRange
fixes #189