Bug 29863 - Segmentation fault in memcmp-sse2.S if memory contents can concurrently change
Paolo Bonzini
pbonzini@redhat.com
Wed Dec 14 17:36:44 GMT 2022
On 12/14/22 15:16, Zack Weinberg via Libc-alpha wrote:
>> The standards are in no way prescriptive in saying that memcmp
>> shall not read or write to memory outside of the input domain.
>
> ... is (as I read it) contradicted by 7.1.4p5 (N1570) "A library
> function shall not directly or indirectly access objects accessible
> by threads other than the current thread unless the objects are
> accessed directly or indirectly via the function's arguments." There
> is more wiggle room in this wording than I'd ideally like, but since
> memcmp has no way of knowing whether any particular piece of data
> outside the ranges supplied as arguments is "accessible by threads
> other than the current thread", it needs to be conservative and not
> touch any of it.
I don't think this applies here for two reasons though:
1) a SIGSEGV would always be acceptable (that's not a valid object), and
so would an infinite loop
2) I think that the as-if rule would even allow reads to objects
accessible by other threads, if they don't affect the result (so they
are not observable by the calling thread) *and* are not observable by
those other threads either. The classic case here is strlen() doing
aligned word (or larger) reads, even though those reads might trespass
the NUL terminator.
Promising any kind of behavior when a data race happens involving the
str*/mem* functions is harder than it seems. As soon as the functions
read a byte more than once, the view of memory that they operate on does
not even obey causality.
The following code is admittedly a bit contrived but shows the pitfalls
of reading more than once from the same location:
while (*(u32*)s == *(u32*)d) {
n-=4, s+=4, d+=4;
if (n < 4) goto short;
}
// we know the loop will stop don't we?
while (*s==*d) s++, d++;
return *s < *d ? -1 : 1;
short:
while (n && *s == *d) s++, d++, n--;
return n ? (*s < *d ? -1 : 1) : 0;
... and you could access beyond the [a,a+n) [b,b+n) range if a
concurrent mutator causes the second while loop to go off the cliff.
There are also compiler issues: it's also hard to ensure that the
compiler won't decide to read twice for very down-to-Earth instruction
selection reasons, for example by using a register-and-memory ALU
operation. Many mem*/str* routines do not have a single memory write,
so the compiler has a *lot* of leeway to reorder and rematerialize
memory accesses. For example you could have something like this in a
memmem():
c = *p;
...
p += table[c];
If the compiler changes the second statement to "p += table1[*p]", for
example to avoid a spill, concurrent mutation can result in
out-of-bounds accesses or other kinds of UB. The standard certainly
didn't require that str*/mem* be written in assembly, or that it does
all of it's accesses as atomic loads or perhaps (argh!) volatile.
Paolo
More information about the Libc-alpha
mailing list