Skip to content

TLS13-C12: per-phase stack boundaries, and the measured reason the gate is red - #1299

Merged
drawmeanelephant merged 2 commits into
mainfrom
agent/autocoder/tls13-c12
Sep 15, 2026
Merged

drawmeanelephant merged 2 commits into
mainfrom
agent/autocoder/tls13-c12

Conversation

@drawmeanelephant

Copy link
Copy Markdown
Owner

Follow-up to #1291. This PR is where the class-B gate finally ran — this host is Apple silicon macOS 27, so vgate.sh builds its own runner and boots the image locally.

It did not pass. It found four real defects, three of which are fixed here, and the fourth of which is a structural property of the client that no host test could have caught.

Fixed

# Defect Evidence
1 _start's frame was 81,264 B; Client is ~87 KiB and was a stack local. The guest died on the prologue store at entry+0x40 and printed nothing. client + seam moved to file scope (_start frame → 208 B)
2 Zig fused the whole call graph into one frame, hiding the cost. noinline serve boundary
3 argv shifted by one. exec.zig's DSK1/DSK3 paths set argc = args.len and pack slot 0 with the first user argument; the ELF path sets argc = 1 + args.len with argv_list[0] = name. The app assumed the ELF shape and read 24533 as an IPv4 literal. serial-contains [fetchs: target set] 0 → 1
4

The open finding, measured

guest task stack (scheduler.task_stack_size)   32,768 B
largest frame in FETCHS.BIN                    79,856 B
second largest                                 34,032 B

The TLS client's stack footprint exceeds the guest stack. Splitting the consumer into noinline phases did not help — the 79 KiB lives inside a single client function, not in the sum of inlined calls.

This is invisible to every host test because the interop driver has an 8 MB stack. It is exactly the class of thing a class-B gate exists to catch.

Why the spec is committed red

live-tls13.spec carries a KNOWN-FAILING block with these numbers. Weakening the assertions would hide the defect. The fix is a genuine decision:

  • Reduce the client's stack use (library restructure), or
  • Grow the per-task stack (kernel-wide; changes every user task's memory footprint).

zig build test: 200/200 steps, 3599/3599 tests. CI does not run class-B gates (no runner registered), so this PR is green where the gate is red — which is itself the point.

The gate runs locally (this host is Apple silicon macOS 27), so for the first
time the consumer executed as a guest instead of only compiling. It found three
faults that no host test could have: the host driver has an 8 MB stack, and
every one of these is a stack or ABI problem.

1. `_start`'s frame was 81,264 bytes. `Client` is ~87 KiB and was a stack
   local; the guest stack is 32 KiB (`scheduler.task_stack_size`), so the guest
   died on the prologue store at entry+0x40 with a data abort and printed
   nothing at all. The client and the seam are now file-scope, the same reason
   the trust store already lived in .bss.

2. `_start` still measured 81,136 bytes after that, because Zig fused the whole
   call graph into one frame. A `noinline` `serve` boundary brought the entry
   frame to 208 bytes and made the remaining cost visible instead of diffused.

3. argv was shifted by one. exec.zig's DSK1/DSK3 paths set `argc = args.len`
   and pack slot 0 with the FIRST USER ARGUMENT; the ELF path sets
   `argc = 1 + args.len` with `argv_list[0] = name`. The app assumed the ELF
   shape, skipped slot 0, and read `24533` as an IPv4 literal -- "bad target".
   It now skips slot 0 only when slot 0 actually is the program name.

Evidence after the fixes (local vgate, artifacts/live-tls13-report.txt):
  serial-contains [fetchs: target set]   = 1   (was 0)
  serial-contains [fetchs: roots loaded] = 0

The run now reaches the consumer body and then faults for a fourth, larger
reason, measured rather than guessed (see the next commit): the client's stack
footprint, ~79 KiB in one frame, exceeds the 32 KiB guest stack.

zig build test: 200/200 steps, 3599/3599 tests.
…te is red

The gate now runs locally. This commit records what it measured, so the next
person gets a number instead of a mystery.

Splitting the consumer into noinline phases did NOT bring the client under
budget -- the 79 KiB lives inside a single client function, not in the sum of
inlined calls. So the finding is stated as a finding:

  guest task stack (scheduler.task_stack_size)   32,768 B
  largest frame in FETCHS.BIN                    79,856 B
  second largest                                 34,032 B

That is a structural property of the client, invisible to every host test
because the interop driver has an 8 MB stack, and exactly the class of thing a
class-B gate exists to catch.

live-tls13.spec carries this as a KNOWN-FAILING block on purpose. It is
committed red rather than relaxed: weakening the assertions would hide the
defect, and the fix is a real decision -- reduce the client's stack use, or
grow the per-task stack (which changes every user task's memory footprint).

zig build test: 200/200 steps, 3599/3599 tests.
@drawmeanelephant
drawmeanelephant merged commit 4b3ca2f into main Sep 15, 2026
9 checks passed
@drawmeanelephant
drawmeanelephant deleted the agent/autocoder/tls13-c12 branch September 15, 2026 12:34
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.

1 participant