Skip to content

fix(rs): an empty path segment is not a container name - #51

Merged
abienkowski merged 9 commits into
mainfrom
fix/empty-container-name-48
Oct 7, 2026
Merged

abienkowski merged 9 commits into
mainfrom
fix/empty-container-name-48

Conversation

@abienkowski

@abienkowski abienkowski commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Closes #48.

Rust's extract_container_name returned Some("") for paths with an empty name segment, so the lifecycle branch treated the empty string as an unknown container and forwarded the request to the Docker daemon. Go and TypeScript denied the same requests. Observed on the RED commit, from the daemon's answers through the Rust proxy:

DELETE /containers/       -> 400 (daemon)   Go/TS: 403 (proxy)
POST   /containers//start -> 301 (daemon)   Go/TS: 403 (proxy)

The fix is one condition in rs/src/proxy.rs: an empty second segment is not a container name.

Spec first, then a failing test, then the fix

The commits are in that order, deliberately:

  1. spec/router.qnt (new). The existing spec routes on :name templates and never defines how a name is extracted from a path, which is why neither Go router doesn't exclude reserved path segments in extractContainerName (cross-language parity) #24 nor this bug could be stated in it. The new module models container-name extraction and the container-lifecycle routing branch over segment lists, with two instances: router (the intended rule) and router_pre48 (pre-fix Rust, ALLOW_EMPTY_NAME = true). A soundness property — a lifecycle route is only ever taken for a real, non-reserved name — holds on router and fails on router_pre48, so the property is load-bearing, not decorative. Eight table-row run tests cover Rust extract_container_name treats empty segment as a container name (DELETE /containers/ is forwarded) #48 and Go router doesn't exclude reserved path segments in extractContainerName (cross-language parity) #24; the same row names appear as comments on the Go, Rust and TypeScript test cases, so every row is traceable across all four. Wired into make typecheck, make test-spec, CI and release-verify.
  2. RED commit (test:). Same-named unit tests in all three languages plus two integration checks. Go and TypeScript passed unchanged; Rust failed exactly the two new tests for the Rust extract_container_name treats empty segment as a container name (DELETE /containers/ is forwarded) #48 reason (left: Allow / right: Deny, left: Some("") / right: None), and Rust's integration run showed the two requests reaching the daemon. This commit is intentionally failing in Rust — see the merge note below.
  3. Fix (fix(rs):). !parts[1].is_empty() added to the existing condition. No Go or TypeScript production code changed; the reserved set stays exactly create/json/exec in all three.

Merge note: please squash-merge. The RED commit fails in Rust by design; a rebase merge would land a failing commit on main.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Implementation(s) changed

  • Go (tests only)
  • Rust
  • TypeScript (tests only)
  • Quint specification
  • CI / infrastructure

Testing

  • Unit tests pass (make test-all)
  • Integration tests pass (make test-integration)
  • Quint verification passes (make verify unchanged; make test-spec covers the new module)
  • New tests added for the change
before after
Go 99 101 (proxy 33 → 35)
Rust 137 139 (proxy 39 → 41)
TypeScript 155 (1 skipped) 156 (1 skipped; proxy 27 → 28)
Integration 30 ×3 32 ×3
Quint run tests 11 + 10 11 + 10 + 9 + 7
$ make test-all            # rc=0
test result: ok. 139 passed; 0 failed
# tests 156  # pass 155  # skipped 1
$ make lint-all            # rc=0
$ make test-spec
  11 passing   10 passing   9 passing   7 passing

All three integration suites pass 32/32 after the fix, with the Rust run showing DELETE /containers/ -> 403 and POST /containers//start -> 403. The RED-state evidence (Rust failing, Go/TS green at 32/32) is reproducible at 642abd2 (check out that commit and run cd rs && cargo test empty).

Non-vacuity of the spec was checked both ways: router_pre48 fails soundTest and both emptyName* rows when they are run against it, and a mutation check (dropping the reserved-set clause) fails soundTest plus the three reserved rows.

Found during review (not in this PR)

Two cross-language divergences surfaced by the whole-branch review, both verified by running the code; follow-up issues to be filed:

  • TypeScript stripAPIVersion only matches undotted versions (/^\/v\d+\//), but the Docker CLI always sends dotted ones (/v1.43/…). DELETE /v1.43/containers/foo, POST /v1.43/containers/beacon/start and POST /v1.43/containers/create are all denied by the TS proxy today. Existing tests miss it because their dotted-version requests are GETs, which the passthrough allows for the wrong reason.
  • Percent-encoding parity: Go decodes DELETE /containers/%2F to an empty name and denies; Rust and TypeScript treat %2F as a literal name and allow — the same empty-name family reached through URL encoding.

Checklist

  • I have read CONTRIBUTING.md
  • My code follows the project's coding style
  • I have updated documentation as needed

@abienkowski abienkowski added Priority: P3 Added to issues and PRs relating to a low severity bugs. Type: Bug Added to issues and PRs if they are addressing a bug labels Oct 7, 2026
@abienkowski abienkowski self-assigned this Oct 7, 2026
@abienkowski
abienkowski merged commit b409e33 into main Oct 7, 2026
6 checks passed
@abienkowski
abienkowski deleted the fix/empty-container-name-48 branch October 7, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority: P3 Added to issues and PRs relating to a low severity bugs. Type: Bug Added to issues and PRs if they are addressing a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rust extract_container_name treats empty segment as a container name (DELETE /containers/ is forwarded)

1 participant