HTTP: a code route can mutate the live route table and static root from a worker thread — unsynchronized writes racing every other worker's lookups
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- c
- Domain
- backend-api-design, networking, testing-qa
Research direction
Start at http_conn_thread and the g_server/eigs_http_active handling, then inspect builtin_http_route, builtin_http_route_authed, builtin_http_static, and the existing serving guard used by http_response_header. Run tests/http_readiness.py and add the live mutation case described in the issue; done means mutation fails loudly, /injected remains 404, and the concurrent probe is race-free under the planned tsan-http lane.
Written by the indexing model from the issue text.
Description
Observed (executed on main a18deac, make http)
A code route runs in a per-connection worker thread. The worker gets its own EigsState, but http_conn_thread then re-points the thread-local server accessor at the SPAWNING server so that handle_request reads the real route table:
/* register_http_builtins allocated a scratch Server on the worker state (so
* http_route/http_serve called inside a code route don't crash) ... Re-point
* at the spawning Server so handle_request reads the main route table. */
eigs_http_active = main_server;
The comment's intent (a scratch server absorbs config calls from a code route) is defeated two lines later: g_server is (*eigs_http_active), so http_route, http_route_authed and http_static invoked from a code route WRITE the main server's config while every other worker reads it lock-free.
Probe:
r is http_route of ["GET", "/", "page"]
c is http_route of ["GET", "/mutate", "code",
"http_route of [\"GET\", \"/injected\", \"INJECTED\"]\nhttp_static of [\"/static\", \"/tmp\"]\n\"mutated\""]
http_serve of 24911
before: GET /injected -> 404
GET /mutate -> mutated
after: GET /injected -> 200 INJECTED
One request permanently changed the routing table for the whole process.
Why it is a concurrency defect, not just a design oddity
builtin_http_routefillsroutes[route_count]'s four string fields, then doesg_server.route_count++— a plain non-atomic int with no release fence. A concurrent worker iteratingfor (i < route_count)can observe the incremented count before the slot's fields are visible and callstrcmpon NULL/garbage.builtin_http_staticswapsstatic_prefix/static_dirunder no lock while workers dereference them (the old strings are leaked rather than freed, so no UAF today — by accident).- The
route_count >= MAX_ROUTEScheck is check-then-act across threads. - Exploitation needs a trusted code route that calls these builtins, so this is a correctness/robustness issue, not a remote hole — but it is a genuine data race in the runtime's most concurrent code, and it lives in the same file that #1137 just cleaned up.
Suggested fix
Freeze configuration at http_serve, the way http_response_header already does: builtin_http_route, builtin_http_route_authed and builtin_http_static check g_server.serving (under response_mu, or make it atomic) and raise a loud error once serving. Every lock-free read of routes/static config is then reading immutable data, which is the property the current code silently assumes.
Gate
tests/http_readiness.py: a live case with a code route that callshttp_route/http_static— the request must fail loudly (500 with the error text) and/injectedmust remain 404 afterwards; plant: revert the freeze → red.- Once #1139 lands a
tsan-httplane, a probe hammering/mutateand/concurrently should report the race on today's tree and be silent after the fix.
Found during the concurrency review that followed PR #1138 (2026-09-14). The rest of the file's threading — init responder, response builder, shared store, per-IP table, config caches — reviewed sound.
- Dominant language
- C
- Stars
- 3
- Forks
- 7
- Avg merge
- 3h 58m
- Merged PRs (30d)
- 105
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from InauguralSystems/EigenScript
-
area:lint-tooling bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
InauguralSystems/EigenScript#1340 ·
Maintainers usually reply within 1 day
-
area:stdlib found-by:code-review kind:silent-wrong
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
InauguralSystems/EigenScript#1338 ·
Maintainers usually reply within 1 day
-
area:lint-tooling found-by:critic kind:docs-drift
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
InauguralSystems/EigenScript#1335 ·
Maintainers usually reply within 1 day
-
area:ci found-by:critic kind:gate-defect
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
InauguralSystems/EigenScript#1311 ·
Maintainers usually reply within 1 day
-
enrolment: decide test_gc_runner_controls.py (exempt vs enrol) and whether floors need a ratchetOpenarea:gates found-by:critic kind:decision
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
InauguralSystems/EigenScript#1280 · 1 comment ·
Maintainers usually reply within 1 day
All issues in InauguralSystems/EigenScript
Similar issues
-
bug needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
netdata/netdata#24062 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
riscv-software-src/riscv-isa-sim#2448 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 1/5 Under an hour Newbie friendliness 75/100
NabuCasa/silabs-firmware-builder#231 · 1 comment ·