Skip to content

libap: ucontext's three switches must be assembly, not C wrappers - #718

Merged
staalmannen merged 1 commit into
mainfrom
claude/upgrade-ape-c-library-mmZGd
Nov 5, 2026
Merged

staalmannen merged 1 commit into
mainfrom
claude/upgrade-ape-c-library-mmZGd

Conversation

@staalmannen

Copy link
Copy Markdown
Owner

The VM refuted the C-wrapper design on the first run. ucontext-test section 0 printed its PASS line once instead of twice and reported `setcontext came back round: got 1 want 2'.

What a C getcontext saves is a point inside getcontext itself. That is fine until the context is resumed after getcontext has returned -- by then the wrapper's frame is dead stack, its own return-address slot has been reused by whatever the caller did next, and the resumed epilogue RETs to that. Here it landed after the setcontext CALL, because setcontext's return address was sitting in the slot, so control appeared to flow straight on with the counter still at 1.

The same reasoning says why swapcontext got away with it, and that half is worth keeping so the lesson is not over-general: a context saved by swapcontext is resumed while its stack is parked, so nothing has run on it and the frame is intact. Sections 1 to 4 all passed. getcontext is the one that returns and is then resumed, and the only one that could have shown this.

All three are assembly now, which is what setjmp.s was all along, and the files are named for what they hold: ucontext.s and makecontext.c. The cost is one shared offset, uc_sigmask at 64, and makecontext.c turns a change to it into a compile error rather than a switch that quietly restores the wrong mask -- verified by swapping two members and watching `size of array _uc_layout_assert is negative'. The mask is handled as sigsetjmp handles it, through _psigblocked(SB), with MOVL because sigset_t is long and kencc's long is four bytes.

Section 5's failure was the test's own, and of the one kind gcc cannot catch: `volatile long a = 0x1111111111111111L' holds 0x11111111 here, so the comparison against the full constant was false and a working switch reported got 0 want 1. long long now. The glibc run could not have found it, because the difference IS the reference's width.

makectx-xcheck follows the rename and stubs setcontext instead of the three primitives; its two frame controls still give 3 and 1.

Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs

The VM refuted the C-wrapper design on the first run.  ucontext-test
section 0 printed its PASS line once instead of twice and reported
`setcontext came back round: got 1 want 2'.

What a C getcontext saves is a point inside getcontext itself.  That
is fine until the context is resumed after getcontext has returned --
by then the wrapper's frame is dead stack, its own return-address slot
has been reused by whatever the caller did next, and the resumed
epilogue RETs to that.  Here it landed after the setcontext CALL,
because setcontext's return address was sitting in the slot, so
control appeared to flow straight on with the counter still at 1.

The same reasoning says why swapcontext got away with it, and that
half is worth keeping so the lesson is not over-general: a context
saved by swapcontext is resumed while its stack is parked, so nothing
has run on it and the frame is intact.  Sections 1 to 4 all passed.
getcontext is the one that returns and is then resumed, and the only
one that could have shown this.

All three are assembly now, which is what setjmp.s was all along, and
the files are named for what they hold: ucontext.s and makecontext.c.
The cost is one shared offset, uc_sigmask at 64, and makecontext.c
turns a change to it into a compile error rather than a switch that
quietly restores the wrong mask -- verified by swapping two members
and watching `size of array _uc_layout_assert is negative'.  The mask
is handled as sigsetjmp handles it, through _psigblocked(SB), with
MOVL because sigset_t is long and kencc's long is four bytes.

Section 5's failure was the test's own, and of the one kind gcc cannot
catch: `volatile long a = 0x1111111111111111L' holds 0x11111111 here,
so the comparison against the full constant was false and a working
switch reported got 0 want 1.  long long now.  The glibc run could not
have found it, because the difference IS the reference's width.

makectx-xcheck follows the rename and stubs setcontext instead of the
three primitives; its two frame controls still give 3 and 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs
@staalmannen
staalmannen merged commit 0fd26c9 into main Nov 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants