Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 61 additions & 15 deletions src/glibc/setjmp/wasm_eh_setjmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,26 @@
* __wasm_longjmp(buf, val) → throw __c_longjmp(&buf[2])
*
* jmp_buf layout (as used by this implementation):
* buf[0] self-reference set by saveSetjmp (used by testSetjmp)
* buf[0] registration token set by saveSetjmp (used by testSetjmp)
* buf[1] unused (reserved)
* buf[2] self-reference set by __wasm_longjmp (payload for throw)
* buf[3] normalised return value set by __wasm_longjmp
*
* The jmp_buf typedef is long int[8] (32 bytes on wasm32), which is
* large enough for all four 4-byte slots used here.
*
* buf[0] holds a per-call unique token rather than buf's own address. This
* matters for jmp_bufs that live in a fixed-address global and get saved and
* restored as raw bytes by their caller (a common unwind-protect idiom built
* on setjmp/longjmp: snapshot sizeof(jmp_buf), memcpy it back later to
* restore an outer context): using buf's address as the token would make
* every registration on that global indistinguishable, since the address
* never changes. A fresh token per saveSetjmp() call means a caller that
* restores an *older* snapshot of the buffer (reverting buf[0] to an outer,
* still-live registration's token) correctly falls through this frame's
* table and resolves in whichever outer frame's table still holds that
* token — matching what raw stack/register snapshot restoration does
* natively. See testSetjmp() for the failure mode this avoids.
*/

#include <stdint.h>
Expand All @@ -32,6 +45,10 @@
* caller via getTempRet0 (wasm has no multi-value imported functions yet). */
static __thread int g_tempRet0;

/* Thread-local monotonic counter used to mint a unique, never-zero token for
* each saveSetjmp() registration (see buf[0] discussion above). */
static __thread int g_setjmp_token;

__attribute__((visibility("default"))) void setTempRet0(int val)
{
g_tempRet0 = val;
Expand All @@ -45,22 +62,24 @@ __attribute__((visibility("default"))) int getTempRet0(void)
/*
* saveSetjmp(buf, label_id, table, size) → new_table
*
* Registers the (buf, label_id) pair in *table* and stores buf's own address
* in buf[0] so testSetjmp can identify the frame later. Grows the table
* (via realloc) when full. Communicates the (possibly new) table size via
* setTempRet0().
* Registers the (token, label_id) pair in *table* and stores a fresh,
* never-zero token in buf[0] so testSetjmp can identify the frame later
* (see the file header comment for why this is a per-call token rather than
* buf's own address). Grows the table (via realloc) when full.
* Communicates the (possibly new) table size via setTempRet0().
*
* The initial table is allocated by the caller (malloc(40), size=4).
*/
__attribute__((visibility("default"))) int *saveSetjmp(int *buf, int label_id, int *table, int size)
{
/* Store self-reference so testSetjmp can match on buf[0]. */
buf[0] = (int)(uintptr_t)buf;
int token = ++g_setjmp_token;
if (token == 0) token = ++g_setjmp_token; /* skip the 0 sentinel on wraparound */
Comment on lines +75 to +76

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In C, signed integer overflow is UB, so the program has already entered UB before we get to the if (token == 0) sentinel check. Maybe we can consider uint32_t in this case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoritically this should never happen, gc heap overflow would happen before token overflow, as roughly ten million setjmp invocation would exhaust gc heap with even 4GB size (maximum possible size), an overflow of token would take 2 billion invocation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would be helpful to include these in the comments because the code explicitly handles this scenario

buf[0] = token;

/* Find an empty slot. */
for (int i = 0; i < size; i++) {
if (table[2 * i] == 0) {
table[2 * i] = (int)(uintptr_t)buf;
table[2 * i] = token;
table[2 * i + 1] = label_id;
setTempRet0(size);
return table;
Expand All @@ -79,7 +98,7 @@ __attribute__((visibility("default"))) int *saveSetjmp(int *buf, int label_id, i
/* Zero the freshly allocated half. */
memset(new_table + size * 2, 0, (size_t)size * 2 * sizeof(int));

new_table[size * 2] = (int)(uintptr_t)buf;
new_table[size * 2] = token;
new_table[size * 2 + 1] = label_id;

setTempRet0(new_size);
Expand All @@ -89,16 +108,43 @@ __attribute__((visibility("default"))) int *saveSetjmp(int *buf, int label_id, i
/*
* testSetjmp(env, table, size) → label_id or 0
*
* Called inside the EH catch block. Searches *table* for a buf whose
* address matches *env* (== buf[0] set by saveSetjmp). Returns the
* label_id of the matching setjmp frame, or 0 if not found (meaning the
* exception belongs to an outer frame and must be re-thrown).
* Called inside the EH catch block. Searches *table* for a registration
* whose token matches *env* (== buf[0], read fresh from the jmp_buf at catch
* time). Returns the label_id of the matching setjmp frame, or 0 if not
* found (meaning the exception belongs to an outer frame and must be
* re-thrown).
*
* *table* is shared by every setjmp() call site within one function
* invocation (see saveSetjmp), so it can hold more than one entry: e.g. when
* a function calls setjmp() on the same jmp_buf more than once (a loop, or
* two call sites merged into one function by inlining), each call appends a
* new entry with a fresh token rather than replacing the old one. Because
* tokens are unique per call, at most one entry can ever match a given env
* value, so search order doesn't matter for correctness; newest-first is
* kept simply because the live registration is usually the most recently
* appended one, making the common case a single-iteration scan.
*
* Entries are intentionally left in the table after a match (not cleared).
* buf[0]/env reflects whatever a caller's raw jmp_buf save/restore (e.g. an
* unwind-protect helper built on memcpy(&saved, &buf, sizeof(buf))) currently
* holds, which may be an older, still-live outer registration's token rather
* than this frame's own — see the file header comment. In that case env
* simply won't match anything in *this* table and correctly falls through to
* be re-thrown to the enclosing frame, which still holds that token in its
* own table. This is what actually resolves an unbounded catch/rethrow loop
* that used to occur when buf[0] was the jmp_buf's own (fixed,
* restore-invariant) address instead of a token: a caller's post-catch
* re-throw to the same buffer kept re-matching this frame's own entry
* forever, exhausting the null-collected GC heap one exception object at a
* time until it crashed with "allocation size too large" instead of
* terminating.
*/
__attribute__((visibility("default"))) int testSetjmp(int env, int *table, int size)
{
for (int i = 0; i < size; i++) {
if (table[2 * i] == env)
for (int i = size - 1; i >= 0; i--) {
if (table[2 * i] == env) {
return table[2 * i + 1];
}
}
return 0;
}
Expand Down