crabc 0.7.6

A Rust musl-compatible libc with a dynamic linker
# Node.js

**Status: ✅ passes under crabc**

| | |
|---|---|
| Package | `nodejs-24.22.1-r0` (Alpine 3.23) |
| Version | `v24.22.1` |
| Baseline | same package with stock musl 1.2.6 |

## What was tested

Node.js combines libstdc++, V8, libuv, OpenSSL, zlib, cares, ICU and
nghttp2 in one address space, creates several worker threads at startup, and
loads native modules with `dlopen`.

- `node -v`;
- a JavaScript program using `process.version`, `os.platform()`;
- `fs.writeFileSync` / `readFileSync`;
- an HTTP GET with the `http` module;
- `zlib.gzipSync`;
- `crypto.createHash("sha256")`.

## Commands

```sh
export PATH=/usr/bin:/bin
node -v
node -e 'console.log("core", process.version, 1+1, require("os").platform())'
node -e 'const fs = require("fs")
fs.writeFileSync("/tmp/n.txt", "hello")
console.log("fs", fs.readFileSync("/tmp/n.txt", "utf8"))'
node -e 'const http = require("http")
http.get("http://127.0.0.1:18080/", r => {
  let d = ""; r.on("data", c => d += c)
  r.on("end", () => console.log("http", d.trim()))
})'
node -e 'const zlib = require("zlib")
console.log("zlib", zlib.gzipSync("hello").length)'
node -e 'const crypto = require("crypto")
console.log("crypto",
  crypto.createHash("sha256").update("x").digest("hex").slice(0, 8))'
```

## Output under crabc

```text
v24.22.1
core v24.22.1 2 linux
fs hello
http hello from crabc workload test
zlib 25
crypto 2d711642
NODE_OK
```

## Baseline comparison

Identical output and exit status under musl.

## Notes

Node was the hardest workload and exposed several loader bugs:

- **TLS symbol resolution.** Undefined TLS symbols (`STT_TLS` with
  `st_shndx == SHN_UNDEF`) were resolved to the *referencing* module instead
  of the defining one, so `__once_call` read from the wrong offset. V8's
  `std::call_once` path crashed at startup until this was fixed.
- **`dlopen` TLS layout.** Appending a module with no previous TLS modules
  could shrink `TLS_TOTAL_SIZE`, making the new block smaller than the old one
  and corrupting the copy. The layout now clamps the append point to the old
  total.
- **`pthread_getattr_np` for the main thread** reported a zero stack base,
  which made V8 think it was running on a foreign stack and attempt a stack
  switch. It now reports the mapping that contains the current stack pointer,
  and the loader gives the main thread an 8 MiB stack (or `RLIMIT_STACK`).
- **Thread TLS lifetime.** V8 keeps pointers into a joined thread's TLS and
  reads them during teardown. Unmapping the block (as musl does) turns that
  into `SIGSEGV`. crabc retains the TLS mapping after `pthread_join`; the
  trade-off is documented in this file and the matrix.

## Known issue

Exited worker threads retain their TLS mapping (`TLS_TOTAL_SIZE + TCB_SIZE`,
about 8 KiB each). The block is no longer used by crabc but stays mapped for
the lifetime of the process. This is required by V8's shutdown behaviour in
Node 24. It does not affect correctness or steady-state memory for a fixed
thread pool.