4 ms·
The point of the magic values are that these values are eventually checked. This should break out of these kinds of optimizations. Edit note: allocators are us
by nyc_pizzadev 5y ago
The point of the magic values are that these values are eventually checked. This should break out of these kinds of optimizations.
Edit note: allocators are user space libraries (stdlib) and not part of the C spec. Use-after-free is extremely unsafe, however its completely valid C.
2nd note: running all programs below give me the expected result, writes to pointers are live, regardless if they are freed. So please provide more concrete steps to reproduce your results.
- geofft 5y agoNo, it should not, because it is impossible for the check to fail in valid C. A use-after-free is invalid C. Type confusion is invalid C. If you have the function void free_ws(struct ws *ptr) { ptr->magic = 0xdeadc0de; free(ptr); } there is no possible circumstance, in valid C, where ptr->magic could be read and have its value equal to 0xdeadc0de. That object is freed immediately after that write. If you do a read in some other function, say char read_from_ws(struct ws *ptr) { if (ptr->magic != WS_MAGIC) { exit(1); } return ptr->s[0]; } it is impossible for the pointer passed into read_from_ws to be a pointer that has passed through free_ws, i.e., it is impossible for ptr->magic to have been set to 0xdeadc0de by free_ws. Therefore, free_ws doesn't need to actually do the write. You're right that the correct magic check is not dead, and cannot be eliminated by the same logic. But the effect there is that the magic check always succeeds!
- vlovich123 5y agoIf you compile with LTO the compiler will see through your obfuscation. Play dangerous games assuming the compiler is stupid, win dangerous prizes.
- DSMan195276 5y agoThe issue is that value is only checked in the context of a use-after-free error though, which the compiler assumes never happens, so it doesn't actually matter to the optimizer. The code I wrote will optimize the same way even if I add a random pointer assignment or read elsewhere (any of which could technically trigger a use-after-free), because the compiler is allowed to assume that because I'm passing that pointer to `free()` I'm never going to use it again. To show this even more, if you add an extra printf to print the value of p after the `free()` (so add a literal use-after-free to check the 'magic' value) the 30 assignment is still gone. It prints zero for me because free() clears that memory, but the assembly does not include the 30 assignment even though I'm clearly reading the value it assigned after the `free()` statement, which is exactly the behavior you're attempting to catch. If `free()` didn't touch the value I'd still see 20. With a little finessing I got my code to print 20 both times (the larger struct gets malloc() to leave the magic value alone) even with the 30 assignment still in the code: struct foo { char bar[35]; int p; }; int main() { struct foo *p = malloc(sizeof(*p)); *(volatile int *)(&p->p) = 20; printf("p=%d\n", p->p); p->p = 30; free(p); printf("p=%d\n", p->p); /* Prints 20 for me, the 30 assignment is optimized out completely */ return 0; }
- DSMan195276 5y ago> Edit note: allocators are user space libraries (stdlib) and not part of the C spec. Use-after-free is extremely unsafe, however its completely valid C. I'm not sure why you think this, but the C standard includes a whole section defining the behavior of the standard library functions, including malloc and free. And it includes this note as undefined behavior: > The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.20.3) So it is undefined behavior to access memory passed to free, or IE use-after-free is undefined behavior if you're using the standard library. > 2nd note: running all programs below give me the expected result, writes to pointers are live, regardless if they are freed. So please provide more concrete steps to reproduce your results. I would check that you're compiling them with `-O2`, and also check the assembly output. However as someone linked, you can already see from the online compiler output that clearly gcc is capable of optimizing the assignment out. Here's my second program in the same compiler, notice how printf is called twice but the 30 assignment (which is what is supposed to set your magic number) is still completely gone: https://godbolt.org/z/9reErcbGK https://godbolt.org/z/9reErcbGK Edit: Sorry, I previously included the wrong quote from the standard (it was about a double-free being UB, slightly different), I have the right one now. I got it from here if you want to look at it, it's a draft but practically the same as the actual one: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
- guerrilla 5y agoThanks for going through all this so meticulously. I actually didn't know this was possible (although I usually disable optimizations, so that might be why.) Just to be clear, using volatile on the magic number will ensure this is avoided?
- DSMan195276 5y ago> Just to be clear, using volatile on the magic number will ensure this is avoided? I think that's your best bet but volatile does bring its own set of problems with it (how it works differs from compiler to compiler). That said it should probably work fine, this is a pretty simple use of volatile. You might check the documentation for your particular compiler though if you have one in mind, it should tell you what it does and if it does anything undesirable (or if there's a better way to do this). Also the volatile cast like I did may be a better approach than actually making the member itself volatile. FWIW, the whole idea is that whether this happens or not has no impact on your program, so in theory you shouldn't ever notice this happens. Detecting use-after-free like this is not really standards compliant so that's a big reason why it's problematic to implement. Also, if you're not using the standard allcator then most of this logic doesn't apply because the compiler won't know your special allocator has the semantics of 'free()'. There are `malloc` attributes in gcc that might trigger similar optimization behavior, but you'd have to be using them, and even then I'm not really sure as I haven't looked into what all they do.