Skip to content

Audit the rest of the codebase; fix a live traversal hole - #9

Merged
jadoon200 merged 2 commits into
mainfrom
feat/capacity-control
Aug 8, 2026
Merged

Audit the rest of the codebase; fix a live traversal hole#9
jadoon200 merged 2 commits into
mainfrom
feat/capacity-control

Conversation

@jadoon200

Copy link
Copy Markdown
Owner

Covers what the M0-M3 and control-layer audits did not: the queueing core, the adaptive
controllers, the Pareto machinery, the price client, and the deployed container. Four
defects, one a security hole in the live image.

1. Path traversal in the deployed dashboard (security)

The SPA catch-all joined the request path onto dist and served whatever it found.
Percent-encoded traversal survives URL normalisation and reaches the handler intact:

/..%2f..%2frequirements-serve.txt   -> served the file
/%2e%2e/%2e%2e/%2e%2e/etc/hostname  -> served the file

Verified against the real deploy image under uvicorn, which also ran as root — so this
was an arbitrary read of the whole container. No credentials exist to steal (the project is
keyless by design), but source, manifests and system files were readable.

Render's edge rejected the encoded forms with a 400, so the live site was not exploitable.
That is an accident of the CDN, not a control, and the README's docker run had no cover.

Fixed: candidates are resolved and confined to dist (closing symlink escapes and
absolute-path injection too), and the image drops to uid 10001. The deploy-image CI lane now
asserts both against the running container.

2. The budget pacer was steering on a frozen capacity trace

adaptive.py kept its own copy of the actuation-delay rule. Fixing the clock-restart bug
in apply_actuation_delay left that copy behind. On a plan that moves every step — what
C5/C6 emit — the mirror reported a flat 13 replicas across 40 steps while the simulator
provisioned 13 → 24. 36 of 40 steps disagreed, so the PI loop corrected against
violations that never happened.

Fixed by extracting ActuationTracker, with the batch form built on it so they cannot
diverge. Tests agree them over 200 random plans, and one fails if the rule is re-implemented
outside simulator.py — this being the second drift of that same logic.

No conclusion moves. Only C5/C6 rows shift, in both directions and by small amounts. Q4
is unchanged at 9 better / 3 tie / 6 worse of 18 with the below-19:1 pattern intact; the GPU
frontier verdict is unchanged at 0 of 8.

3. Hypervolume measured the wrong staircase

It swept from cost zero and credited each strip with the dearer point's violation rate,
when only the cheaper point is affordable there. One point at cost 5 with 0.2 violations
against a reference of 10 returned 8.0 where the rectangle is 4.0 — and the error can
reverse a ranking, not just inflate both sides. The old test compared two curves where it
cancelled. Nothing published used the function.

4. The two fidelities were documented as the same system

utilisation_target derates the utilisation model but not the queue, so at 0.8 the queue
runs at rho = 0.8 where the utilisation model reports a step at its limit. Harmless — every
published result uses the utilisation model, and Erlang-C validation sets the target to 1.0 —
but the docstring asserted something false about the code.

Verified sound

ACI matches the published BACC update including its sign under miscoverage; pareto_front is
a correct two-objective sweep; cost_at_matched_violation reports unreachable targets rather
than substituting the nearest point; the price parser skips non-positive prices rather than
defaulting them. OData filters are now escaped — not a live injection path, but an apostrophe
would have returned the wrong SKU's price.

Gate

ruff clean; mypy strict clean; 201 tests; make validate-simulator GREEN.

Covers what the M0-M3 and control-layer audits did not: the queueing core, the
adaptive controllers, the Pareto machinery, the price client and the deployed
container. Four defects.

The budget pacer was steering on a frozen capacity trace. adaptive.py kept its own
copy of the actuation-delay rule so C5/C6 could pace against their own past
requests, and fixing the clock-restart bug in apply_actuation_delay left that copy
behind. On a plan that moves every step — which is what C5/C6 emit — the mirror
reported a flat 13 replicas across 40 steps while the simulator provisioned 13 to
24, so the PI loop corrected against violations that never happened. Extracted
ActuationTracker and implemented the batch form on top of it so the two cannot
diverge; a test agrees them over 200 random plans and another fails if the rule is
re-implemented anywhere outside simulator.py. Re-running the frontier moves only
C5/C6 rows by small amounts in both directions. Q4 is unchanged at 9/3/6 with the
below-19:1 pattern intact, and the GPU frontier verdict is unchanged at 0 of 8.

The SPA catch-all could be walked out of the dist directory. Percent-encoded
traversal survives URL normalisation and reached the handler intact, serving
arbitrary container files — verified against the real deploy image under uvicorn,
which also ran as root. No credentials exist to steal, the project being keyless,
but the source and every system file were readable. Render's edge rejected the
encoded forms with a 400, so the live site was not exploitable; that is an
accident of the CDN, not a control, and the README's docker run had no such cover.
Candidates are now resolved and confined to dist, which also closes symlink
escapes and absolute-path injection, and the image drops to an unprivileged user.
The deploy-image CI lane asserts both against the running container.

dominated_hypervolume swept from cost zero and credited each strip with the dearer
point's violation rate, when only the cheaper point is affordable over that
interval. One point at cost 5 and 0.2 violations against a reference of 10
returned 8.0 where the rectangle is 4.0, and the error could reverse a ranking
rather than merely inflate it. The old test compared two curves where it
cancelled. Nothing published used the function.

_synthesize_arrivals claimed the utilisation and queue fidelities described the
same physical system. They do not — utilisation_target derates one and not the
other — which is harmless because every published result uses the utilisation
model and the Erlang-C validation sets the target to 1.0, but the docstring
asserted something false.

Verified sound: the ACI update matches the published BACC form including its sign,
pareto_front is a correct two-objective sweep, cost_at_matched_violation reports
unreachable targets rather than substituting the nearest point, and the price
parser skips non-positive prices rather than defaulting them. The OData filter is
now escaped; not a live injection path, but an apostrophe would have returned the
wrong SKU's price.
Caught by CI, not locally, because I read ruff's trailing summary line as
success instead of checking its exit code. The gate is a set of exit codes and
should be verified as one.
@jadoon200
jadoon200 merged commit d58464e into main Aug 8, 2026
3 checks 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.

1 participant