-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[RFC] Dynamic check for naked pointers #9534
Conversation
This looks great! One change that might reduce false positives: I think it's invalid to create out-of-heap pointers that aren't black, regardless of whether they have size 0. (One reason for this is because the GC will mutate non-black headers, which probably isn't what you want on out-of-heap data, and might segfault if the out-of-heap data is mapped readonly) This should make the test a bit more robust: not that many words of memory are black headers (you need a 1 bit at a specific location), but an awful lot of memory happens to be zero. One other check you should consider is checking that the size is valid. Sizes of above, say, 2^40 words are definitely not valid headers. |
Should these be |
Huh, right. I think it would make more sense to have them be black, as you say. |
By all means, let's create atoms in the atom table with black color, if that's more convenient. There's no special reason atoms are white today: it makes no difference in "naked pointers" mode. I see that ocamlopt already uses the black color for statically-allocated blocks containing structured constants. It makes complete sense to do the same for the atom table. |
Concerning sanity checks on possible headers: I agree with putting an upper bound on the size field. We could also try to check the size against the tag, e.g. a |
@garrigue I ran
The output is a bit noisy with the same warning repeated for several major GC rounds. |
Given that this requires the last byte to have |
@kayceesrk Thank you, I'll investigate those. |
see ocaml/ocaml#9534 for more discussion on the subject.
Done; ocaml/opam-repository#16357 -- I'll post a note to discuss.ocaml.org to encourage more testing when it's merged. When merged, you can use it simply by:
|
Is NULL a naked pointer, or is there a special case for it ? |
Also, is there some kind of dummy header I could use for creating a static tuple on the C side ? |
Regarding NULL: @lpw25 and I were discussing that we should probably add |
In this case, you could use |
You can use @mshinwell it might be useful to expose a stable macro for a static header. |
I would naive assume that it is fairly common in C bindings to sometimes store NULL instead of a valid pointer into the OCaml heap (can we know?). Wouldn't it make sense for the GC to support (and ignore) NULL pointers? This may fit nicely with the current |
I think we should separate out two items here
Using a tagged integer instead doesn't fill the same role. Currently you can store either an OCaml value or null into a block. You know that those two things are disjoint and allowed by the runtime. |
@gasche That is what I'm suggesting. Effectively it would amount to using an unboxed sum type @kayceesrk Yeah, we should expose a macro for a static header and recommend that in the manual, most likely. |
I don't have strong opinions against |
I've created #9535 for |
This looks pretty good, thanks! Are you interested in a proper review or not? |
@xavierleroy Happy to get a proper review! |
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 looks good to me, and should be very useful to find uses of "naked pointers" in the wild. Thanks!
Two minor comments below. No changes required.
runtime/major_gc.c
Outdated
tag_t t; | ||
|
||
Caml_state->checking_pointer_pc = &&on_segfault; | ||
h = Hd_val(v); |
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, Sir, is a bold use of GCC's first-class labels, but I kind of like it.
The more conservative approach would sigsetjmp
here and siglongjmp
out of the signal handler for SIGSEGV, but maybe we found out previously that it doesn't work.
I'm just hoping the optimizer understands that unpredictable control flow can occur.
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 optimizer doesn't if the declaration header_t h
isn't marked volatile. I haven't tried sigsetjmp/siglongjmp
. I shall give that a try shortly.
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'm afraid you'll find sigsetjmp
and siglongjmp
too costly (they work with signal masks via system calls?). The current code is very efficient; all we need it to get it to work reliably despite GCC and Clang optimizations...
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.
You are right. sigsetjmp
and siglongjmp
is quite slow. I tried opam install frama-c -y
on a fresh switch. With GCC first-class labels, it takes 3m30.726s
. With sigsetjmp/siglongjmp
it takes 16m2.556s
.
runtime/major_gc.c
Outdated
return 1; | ||
|
||
on_segfault: | ||
if (Caml_state->checking_pointer_pc == 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.
Maybe I'm missing something obvious, but it's unclear to me why on_segfault
is here and not on line 186 below. On a related note, why are we re-testing checking_ponter_pc
?
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 needed this to get the optimizer to generate the correct code. I agree that this is quite brittle.
checking_pointer_pc
is reset at line 160 if there was no segfault. The segfault handler doesn't reset it. I'll give sigsetjmp/siglongjmp
a try as it might be the better way to do this.
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.
OK, I suspected an issue with optimizations.
There may be a way to turn optimizations off for this function, using GCC/Clang attributes.
runtime/major_gc.c
Outdated
Caml_state->checking_pointer_pc = NULL; | ||
fprintf (stderr, "Out-of-heap pointer at %p of value %p. " | ||
"Cannot read head.\n", p, (void*)v); |
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 was naively expecting on_segfault
to jump right 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.
Same as above.
I have disabled optimizations for |
I played a bit with the approach and, yes, it is hard to make GCC and Clang produce the expected assembly code, even in One possibility is to use inline assembly for the critical bits... Since this is goign to run under x86_64 only, it might be acceptable. If you're interested I have some asm code that I could adapt. |
That would be useful. Please do share. |
Here is what I had in mind. First, the "load and trap segv" part, written in assembly:
This could also be written in true asm syntax in runtime/amd64.S, but then it would not be inline. Now we can use this in
|
I've incorporated the inline assembly suggestion. Tested on GCC and Clang and confirmed that they work as expected. As a next step, I suppose we should announce this more widely. @avsm volunteered to post it on discuss.ocaml.org so that there is wider testing. We should also close this PR as it is not meant for merging. |
Closing as suggested by @kayceesrk . Looking forwards to having this as an OPAM switch! |
Posted on discuss.ocaml.org for wider visibility https://discuss.ocaml.org/t/ann-a-dynamic-checker-for-detecting-naked-pointers/5805 |
Thank you very much for this! This sort of thing is a significant help to know what the situation is between existing code and upcoming changes, which is an immensely better situation to be in than just guessing how much code needs to be reworked and retuned. It isn't useful so far, but I just wanted to mention that I have been doing some testing with this. Unfortunately, so far I have failed to get it to trigger where, as far as I understand, it should, which leaves me unsure of the other non-reports of problems. I will work on narrowing things down. |
Hi @jberdine, I am happy to look at this if you have example. |
@nojb @kayceesrk @xavierleroy @dra27 Here's an implementation of #include <stdio.h>
#include <windows.h>
typedef unsigned int value;
int safe_load(volatile value* p, value* result) {
value v;
__try {
v = *p;
}
__except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
*result = 0xdeadbeef;
return 0;
}
*result = v;
return 1;
}
int main() {
value foo = 42;
value* ps[] = { &foo, NULL, (value*)438294732 };
for (int i = 0; i < 3; i++) {
value res;
int r = safe_load(ps[i], &res);
printf("%p: %d %08lx\n", ps[i], r, res);
}
} |
@stedolan Thank you! I confirm that with this function the dynamic check seems to works under msvc. (It would be nice to have this as a configure option.) |
Thanks to @dra27, the support for mscv64 has been merged into kayceesrk/ocaml/4.10.0+nnp+check branch. |
Note: This PR is not for merging, but to aid discussion
This PR adds the ability to dynamically identify naked pointers to 4.10.0 compiler. A naked pointer is identified during a GC if the pointer is either
If the block is zero-sized or marked black, then the GC would not have to scan this object and can be safely skipped. Of course, this technique will have false negatives since the bit pattern in the field where the header is expected to be may look like a zero-sized block or marked black.
When a naked pointer is found, a warning is output to
stderr
and the program continues.OPAM remote
You can install this compiler using this opam repo:
Example
Finding the source of the naked pointers
Given that the naked pointers are discovered during the GC, how does one find the source of the problem?
rr
makes it quite easy. Let's look at a debugging a naked pointer on a real project:This corresponds to the naked pointer at https://github.com/Frama-C/Frama-C-snapshot/blob/master/src/libraries/datatype/unmarshal.ml#L72.
Limitations
amd64
on Linux.rr
for locating the allocation point. There may be clever ways to usegdb
to do the same.