Skip to content

sdk: reserve an 8 MiB main-thread shadow stack for linked programs - #842

Closed
brandonpayton wants to merge 2 commits into
mainfrom
sdk/main-thread-stack-8mib
Closed

sdk: reserve an 8 MiB main-thread shadow stack for linked programs#842
brandonpayton wants to merge 2 commits into
mainfrom
sdk/main-thread-stack-8mib

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 4, 2026

Copy link
Copy Markdown
Member

Why

WebAssembly programs use a shadow stack in linear memory for ordinary C and C++ function calls. If that stack is too small, calls can overwrite unrelated allocator and thread-local-storage data before the runtime notices a failure. wasm-ld is the linker that lays out this memory.

Prevent ordinary deep C/C++ call chains from silently overflowing wasm-ld's roughly 64 KiB default main-thread shadow stack into Kandelo's allocator and pthread/TLS globals.

This is an SDK memory-layout defect, not a package compatibility workaround. It was reproduced by ncurses terminfo loading and GTK image loading, where the eventual trap occurred far from the actual stack overwrite.

What changed

A response file is a text file whose arguments are expanded by the compiler or linker. Because callers can put stack-size options inside nested response files, both SDK drivers must inspect them before enforcing the safe minimum.

  • Reserve an 8 MiB main-thread shadow stack for executable links in both SDK drivers.
  • Treat 8 MiB as a floor: smaller explicit requests are raised, while valid larger requests remain effective.
  • Ask pinned Clang/Clang++ for the real -### job graph before changing executable links. Compiler-only modes remain compiler-only; executable links require exactly one pinned wasm-ld job.
  • For the TypeScript SDK, parse the exact quote-aware wasm-ld argv and its LLVM 21 driver-owned working-directory slot.
  • For the Kandelo-native Bash SDK, classify the real Clang job graph before its direct wasm-ld path and inspect the already-classified linker arguments.
  • Expand nested response files iteratively in both drivers. Canonical cycles, missing files, malformed encodings, and explicit resource ceilings fail before linking; no readable response content can be silently skipped.
  • Support LLVM's UTF-8/UTF-16 response decoding in the TypeScript driver. The self-contained native driver rejects UTF-16 before output because it has no declared transcoder.
  • Preserve user linker arguments unchanged and append only the computed SDK floor.

POSIX leaves the default stack size implementation-defined. 8 MiB matches the common Linux/glibc main-thread default. This affects only the initial thread; pthread stacks continue to use musl's separate default.

Compatibility

Valid compiler-only actions remain intact in both drivers, including direct and response-file forms of:

  • -fsyntax-only
  • -M
  • -MM
  • --analyze

Response expansion is bounded at 4,096 files, 1,048,576 tokens, and 64 MiB of decoded text. Exceeding a bound fails explicitly before output instead of risking a silently lowered stack request.

ABI

No ABI change. The host discovers each module's exported __heap_base and __stack_pointer dynamically. Existing binaries and newly linked 8 MiB binaries remain compatible with the same kernel, and the ABI snapshot is unchanged.

Exact validation

Exact candidate: 457188e2e065eb70d6cf73b41987c5e90cf12f15

Tree: d777e3867d78129b58a08b4dff2331ab117799e0

Parent: #913 head c6849bde6cfbe51837227789840ecf2b05791070

  • Full SDK suite passed: 8 files, 94 tests.
  • Real LLVM 21 Node and native links produced the expected 8 MiB and 16 MiB stack layouts.
  • Repeated response includes across separate -z operand boundaries preserved a 32 MiB request in both drivers.
  • A 100-file nested response chain preserved a 16 MiB request in both drivers.
  • A 4,097-file chain failed before output in both drivers at the documented 4,096-file ceiling.
  • Direct and top-level-response forms of all four compiler-only modes passed through the real Node and native entrypoints.
  • Direct/config/environment working directories, -ffile-compilation-dir, late -Xclang spoofing, object-only links, and filenames containing newlines were covered.
  • Cycles, symlink-alias cycles, missing files, UTF-16 handling, invalid numeric forms, overflow, zero/foreign/duplicate linker jobs, and malformed/ambiguous traces were covered.
  • ABI snapshot/header/TypeScript consistency passed.
  • Dev-shell and macOS Bash 3.2 syntax passed.
  • git diff --check passed.
  • Independent review accepted this exact tree after rejecting earlier candidates for a depth-64 silent-lowering bug and compiler-only mode regressions.

Merge order

This PR is stacked directly on #913. Merge #913 first, then retarget this one-commit PR to main.

This is a main-repository PR for review and must not be auto-merged.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-842-staging

ABI v39. 71 built, 0 failed, 71 total.

Package Arch Status Sha
icu wasm32 built 297a2f0c
libcurl wasm32 built 47b2440d
libcxx wasm32 built c6cce11e
libcxx wasm64 built 1407cb4b
libiconv wasm32 built 86049231
libpng wasm32 built cffa4555
libxml2 wasm32 built 2a413334
libzip wasm32 built 6521d0d2
openssl wasm32 built d3cb845b
openssl wasm64 built 044b1b8c
sqlite wasm32 built ba7f72f5
sqlite wasm64 built 0c343dd8
zlib wasm32 built 4f716fce
zlib wasm64 built b5de4194
bc wasm32 built b9fe6182
bzip2 wasm32 built 5bb39152
coreutils wasm32 built 6dd28e2c
curl wasm32 built 982dd0cc
dash wasm32 built 7ec1a402
diffutils wasm32 built fb7449d7
dinit wasm32 built 7863be5d
fbdoom wasm32 built c1deba45
file wasm32 built f6e0d0e7
findutils wasm32 built 97692b4a
gawk wasm32 built e57e8cf8
git wasm32 built 997a3170
grep wasm32 built be691a28
gzip wasm32 built ab013af5
hello wasm32 built fc9eb295
kandelo-sdk wasm32 built 95370058
kernel wasm32 built d2b8f035
less wasm32 built d5cbbf8b
lsof wasm32 built 7f55101c
m4 wasm32 built 5b0cae14
make wasm32 built 281b19d7
mariadb wasm32 built 5813cfca
mariadb wasm64 built 295e8dff
modeset wasm32 built ee391a0a
msmtpd wasm32 built 11ce4a72
nano wasm32 built af164b1f
ncurses wasm32 built 0cd7f951
netcat wasm32 built 1c419f24
nginx wasm32 built 684016cd
php wasm32 built d1b310cf
posix-utils-lite wasm32 built 7909e25d
ruby wasm32 built 9bc6deab
sed wasm32 built 61ffc7ee
spidermonkey wasm32 built 9664571c
tar wasm32 built 2f3a42cc
tcl wasm32 built 36656326
unzip wasm32 built 912c5251
userspace wasm32 built 0b8c637e
vim wasm32 built e0218a9b
wget wasm32 built 6ac5c9d9
xz wasm32 built 79ade065
zip wasm32 built 0d88dadb
zstd wasm32 built 3fb34310
bash wasm32 built 35d15ecf
mariadb-test wasm32 built 4bbce5c4
mariadb-vfs wasm32 built cc7c3a93
mariadb-vfs wasm64 built 4dffb637
nethack wasm32 built 43da0b72
node wasm32 built 734d6390
spidermonkey-node wasm32 built 54bba4a1
vim-browser-bundle wasm32 built f018db55
nethack-browser-bundle wasm32 built 1ac8b0d1
rootfs wasm32 built 78f431ee
shell wasm32 built a6fc5295
lamp wasm32 built 3c76784e
node-vfs wasm32 built 81978a15
wordpress wasm32 built ae3a8ba0

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

@brandonpayton
brandonpayton force-pushed the sdk/main-thread-stack-8mib branch 2 times, most recently from cdff91f to 2dc2115 Compare July 11, 2026 00:23
This was referenced Jul 11, 2026
Keep parser classifications for SDK mode and thread inference, but forward non-SDK arguments in their original sequence so objects, -l libraries, archives, and linker group controls retain normal static-link semantics. Add unit and real wasm-ld coverage for mixed input ordering and linker controls.
@brandonpayton
brandonpayton force-pushed the sdk/main-thread-stack-8mib branch from 2dc2115 to 39f72a3 Compare July 14, 2026 03:00
@brandonpayton
brandonpayton changed the base branch from main to fix/sdk-preserve-linker-order July 14, 2026 03:00
@brandonpayton
brandonpayton force-pushed the sdk/main-thread-stack-8mib branch from 39f72a3 to b3584f0 Compare July 14, 2026 09:34
@brandonpayton
brandonpayton marked this pull request as draft July 14, 2026 09:35
@brandonpayton

Copy link
Copy Markdown
Member Author

Exact-head review after the push found one additional linker-trace path bug, so this PR is draft again and b3584f0 is not the final candidate.

With -working-directory DIR -Wl,@relative.rsp, Clang resolves the response file relative to DIR, while the scanner on this head resolves it from the parent process cwd. A real link therefore accepted a 16 MiB request but the SDK appended 8 MiB afterward.

The correction will derive the effective working directory from Clang's actual quote-aware cc1 trace, require all emitted compile jobs to agree, resolve remaining lld response paths from that directory, and add a real regression. I will update the exact SHA/evidence and mark the PR ready only after another independent review.

WebAssembly has no shadow-stack guard page. wasm-ld's roughly 64 KiB default lets deep ordinary call chains overwrite adjacent .bss state, including Kandelo's pthread and TLS globals, before failing later in unrelated code. Reserve 8 MiB, matching the common Linux main-thread default, while preserving any valid larger stack requested by the caller.

For Node-hosted commands, first ask the pinned Clang or Clang++ driver for its real -### job graph without injecting SDK glue. Compiler-only modes such as -fsyntax-only, -M, -MM, and --analyze then bypass executable preparation even when they arrive through response files. Confirmed executable links get a second trace with the complete SDK inputs; exactly one pinned wasm-ld job is required, while the wrapped toolchain's expected wasm-opt post-link job remains valid. The Kandelo-native driver applies the same job-graph classification before its direct-link path.

Compute the effective stack floor from the exact pinned wasm-ld argv. Relative lld responses use the source-proven driver working-directory slot in LLVM 21's cc1 command. Response expansion is iterative rather than depth-limited: both drivers reject missing files, active canonical cycles, or explicit file/token/text resource limits before linking, and inspect 100-level LLVM-accepted chains without silently lowering a larger request. The Node path decodes LLVM-compatible UTF-8 and BOM-marked UTF-16; the Kandelo-native driver rejects UTF-16 explicitly because Bash cannot retain embedded NUL bytes and the packaged SDK declares no transcoder.

Cover both drivers with parser, malformed-input, response-file, fail-closed trace, and real LLVM tests. Direct and response-file forms of all four compiler-only modes run through the actual Node and Kandelo-native entrypoints, with their outputs confined to temporary directories. Executable coverage includes interleaved optimization, include-path, PCH, framework, working-directory, deep-chain, cycle, and alias-cycle cases.

The Node path pays for two non-compiling Clang traces per executable link and one for an otherwise-unrecognized compiler-only mode; the native path pays for one classification trace before a possible link. No ABI bump is required because this changes user-program link layout only; the host reads each module's exported stack and heap positions dynamically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonpayton
brandonpayton force-pushed the sdk/main-thread-stack-8mib branch from b3584f0 to 457188e Compare July 14, 2026 10:34
@brandonpayton
brandonpayton marked this pull request as ready for review July 14, 2026 10:34
Base automatically changed from fix/sdk-preserve-linker-order to main July 14, 2026 18:42
@brandonpayton

Copy link
Copy Markdown
Member Author

Absorbed by rebase-merged #967. Main is now 6efb411 with the sealed tree b133bfa018f7e2bc84c4845edf2f945b2f844954, and ABI 41 activation completed successfully in run 29550069085. The absorption audit confirmed this PR\x27s behavior and tests are present in that merged tree, so this source PR is closed and its branch is deleted.

@brandonpayton
brandonpayton deleted the sdk/main-thread-stack-8mib branch July 17, 2026 02:38
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