[runtime] Stricter exn_handler processing#13207
Conversation
The invoked code may end up causing caml_try_realloc_stack() to be invoked, which in turn may replace the stack TRAP_PTR points to, leading to either crashes with the debug runtime (thanks to the old stack contents being overwritten) or all kinds of memory or control flow corruption otherwise.
|
Well spotted! You're correct that in OCaml 5, unlike in OCaml 4, growing the OCaml stack changes the value of the exception pointer (the address of the first exception handler in the stack), so However, there's no need for "upon return from the invoked C code, there is an exception pending", nor for the debug runtime system to be used: as soon as On a Mac M1, I'm getting a reproducible "bus error" with the modified test below and when setting |
|
Thanks @xavierleroy for that simple reproducer case! I'll borrow it, but with a smaller recursion bound to prevent failure with a |
|
Note: if I understand correctly, all 64bit backends are affected, except amd64{,nt} where the trap pointer is not cached as a register. (I suppose this is because amd64 have less registers available than other isas.) This is probably a small mistake that was copied from one backend to the next when they were adapted to Multicore. |
gasche
left a comment
There was a problem hiding this comment.
I reviewed the fix and believe it is correct.
|
Caching the trap pointer makes try/raise slightly faster (one less read from memory, although Caml_state is hot so certainly in cache), and it makes C calls slightly slower (one extra register to spill); it costs one register. I suppose this is worth it because we assume that typical OCaml code may rely on exceptions being fast, and does not use C calls that often. |
Your understanding is correct. |
I agree that the fix should be backported to 5.2. If the backport to 5.1 is straightforward, it would make sense to have the fix in the 5.1 maintenance branch, but I am not expecting a 5.1.2 release any time soon (because I would rather focus on 5.2.1). |
|
The change is pretty simple for each architecture (it's one line missing), so this should be easy to backport. It will probably not backport automatically due to some architectures being absent in earlier releases. I agree that backporting to 5.1 has unclear benefits -- I don't see any reason for users to stick to 5.1 instead of moving to more recent releases. |
Actually, I had checked and all the .S files already exist in 5.1. But it is not worth applying the changes to |
|
I'm running a "CI precheck" on this PR, so that the architectures impacted by this PR are all tested. |
I think this is beneficial for target architectures having 32 integer registers, where it's no big deal to reserve a register to hold the exception pointer. With 16 integer registers, this is probably not true. So, for s390x, it could make more sense to not keep the exception pointer in a register. |
|
(Once the precheck CI results are in, I think that it would reasonable to go ahead, merge, and backport to 5.2; but I will leave this decision to @xavierleroy. If we want to consider slightly more invasives change to s390x this can come as a further PR.) |
xavierleroy
left a comment
There was a problem hiding this comment.
Looks good to me. CI precheck is positive. I'll merge next.
|
5.2 backport: 1e8a91d |
This innocent-looking PR is intended to fix the issue noticed by @polytypic, and (very painfully) reproduceable using his testcase. (he did not open an issue for this in the OCaml project)
The bug being fixed here, is that, on platforms where the current value of the domain's
exn_handleris kept in a register for speed (i.e.TRAP_PTR), reloading the register after its value may have changed has been missed incaml_c_calland/orcaml_c_call_stack_args.This wasn't noticed because:
And then, as long as the debug runtime is not used and the exception is not reraised,
TRAP_PTRwill eventually return to a correct value without causing havoc.Which is why this was so hard to reproduce (and of course, amd64 and s390x were not affected). With @polytypic's test case, I have been able to reproduce quite reliably, using the debug runtime, on OpenBSD/arm64, thanks to scheduler or
libpthreadbehaviour on this platform causingcaml_unix_readto return with an exception at the end of the test program, reasonably often (it nevertheless takes about 200 runs of the test program to reach this).Also, since amd64 does not save this value into a dedicated register, other runtime .S files which have been using amd64 as inspiration could easily miss this - in fact it was more surprising to see s390x handle this correctly (at least in the
caml_c_callcase).This fix should ideally be backported to 5.1.x and 5.2.x.