Repository navigation
Deferred minor findings from the #43/#47/#50/#51 review cycles #55
Copy link
Copy link
Labels
Type: MaintenanceAdded to issues and PRs when a change is for repository maintenance , such as CI or linter changes.Added to issues and PRs when a change is for repository maintenance , such as CI or linter changes.
Description
Problem
The review processes for #45 (PR #47), #24 (PR #43), #39 (PR #50) and #48 (PR #51) produced a set of deliberately deferred Minor findings — real but not merge-blocking. Recording them here so they stop living only in session logs.
Cross-language parity (smallest first)
- HEAD on a named container:
HEAD /containers/x— Go and TS allow via the GET/HEAD passthrough; Rust denies in its lifecycle branch (rs/src/proxy.rscatch-all). Related to Routing parity: TS path-wide exec deny; Go matchEndpoint accepts endpoint subpaths #49. - Numeric gid edge parity (
--listen-socket-group): negative values — Go rejects with "negative gid", Rust/TS fail via name lookup; leading+— all three now reject (post-feat!: dockerd-parity listening socket, Unix path only #47 fix) but via different paths; oversized values fail differently per language. Behaviourally all deny; messages differ. - Name-lookup gid range: Go's pure-Go resolver and TS's
/etc/groupparser don't range-check a gid obtained by name lookup (the digits-only path does). Exploiting it requires control of/etc/group. - Probe "other connect error" messages differ by OS error wording (Go
connect: permission deniedvs RustPermission denied (os error 13)vs Nodeconnect EACCES); tests assert therefusing to removeprefix only. - Linux full-backlog probe: a live listener with a full backlog → Rust times out → "in use by another process"; Go gets EAGAIN → "refusing to remove … resource temporarily unavailable". Both safe (nothing unlinked); message diverges.
Listener (from #45's reviews)
- macOS full-backlog live listener can return ECONNREFUSED → probe treats it as stale (only lockless peers exposed; Go/Rust hold the flock).
Lstat→Removewindow: a lockless process's swapped-in file could be removed (tiny race, design accepts it).- Rust leaves the 0600 socket file on disk when chown/chmod fails (Go's Close unlinks); pre-existing parity gap.
- SIGKILL test child can leak on
t.Fatalin Go (addt.Cleanup); GC test passes partly by construction. - Concurrency-test losers in Go assert a substring ("is in use by another"), Rust asserts the exact message.
- EPERM tests could also skip when
getegid()==0.
Tests & tooling
- Integration checks assert HTTP status only; asserting the deny body ("not allowed") would stop a daemon-side 403 masking a proxy regression (no authz plugin in the test stack today, so theoretical).
- Rust/TS route-table tests stop at the first failing row; Go uses
t.Runper row. - Go's
extractContainerNameempty-string test documents the contract but can't fail for the Rust extract_container_name treats empty segment as a container name (DELETE /containers/ is forwarded) #48 cause (""is both "no name" and the raw value). - Go flag-removal test builds the binary in-test; TS
VALUE_FLAGS/BOOL_FLAGSexported mutable. ROUTER_SPEC :=vs?=used by the other Makefile spec vars.test-sock.shhas a duplicate "GET /_ping -> 200" label (granted vs default-group).
Docs
- README test table shows "+ 32 integration" on the Go row only, though the suite runs per implementation; the 15 socket tests aren't in the table.
- README "all 9 invariants" sentence predates the listener/router modules.
- spec/README's "tracked with the other routing divergences" (exec note) could cite Routing parity: TS path-wide exec deny; Go matchEndpoint accepts endpoint subpaths #49 directly.
spec/listener-design.mdstill has pre-feat!: dockerd-parity listening socket, Unix path only #47 group-table wording in one historical section (fixed in the normative table; the history records the old plan).
Proposed solution
Work through these in one or two chore:/docs:/test: PRs, or pick items off when touching the files anyway. None changes behaviour except the parity items, which should each get the #24/#48 treatment (decide canonical row, converge, pin with same-named tests).
Which implementation(s) would this affect?
- Go
- Rust
- TypeScript
- Quint specification
- CI / infrastructure
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Type: MaintenanceAdded to issues and PRs when a change is for repository maintenance , such as CI or linter changes.Added to issues and PRs when a change is for repository maintenance , such as CI or linter changes.
Type
Fields
Priority
None yet
Activity
abienkowski commentedon Oct 7, 2026
Adding a cross-language item found while pinning #52 (verified by running the Go router; Rust by reading
strip_api_version,rs/src/proxy.rs:200-207):v(Gorouter.go:161-169, Ruststrip_api_version), not just/v<digits>[.<digits>]/. Observed (Go router, main8865134):/v<major>and/v<major>.<minor>and denies all three;ts/src/proxy.test.tspins that as TS-only rows./volumes/…, whose DELETEs are otherwise denied) is classified as a container lifecycle call and forwarded with its original path. The daemon then routes it as a volume request. The impact looks low because volume names cannot contain/, so the daemon should refusecontainers/fooas a volume name. That has not been checked against a live daemon, so treat this as worth a Go router doesn't exclude reserved path segments in extractContainerName (cross-language parity) #24/Rust extract_container_name treats empty segment as a container name (DELETE /containers/ is forwarded) #48-style convergence (canonical rule^/v\d+(\.\d+)?/, same-named rows in all three languages), not just a doc note.abienkowski commentedon Oct 7, 2026
The API-version over-strip item is now tracked as its own bug: #57 (P2). Docker confirmed that
DELETE /volumes/containers/foois handled as a volume removal, while Go and Rust classify it as a container delete.abienkowski commentedon Oct 7, 2026
One more item: no formatter check in
make lint-*.lint-goruns onlygo vet,lint-rsonlycargo check, andlint-tsonlytypecheck. Onmain(a654ff8),gofmt -l go/internal/proxy/listsrouter.goandtransport_test.go,ts/src/proxy.test.tsfailsprettier --check, andcargo fmt --checkreports diffs. Proposal: one formatting commit per language, then addgofmt -l(fail if it prints anything),cargo fmt --checkandprettier --checkto the lint targets and CI. Keep it separate from behaviour changes so the diffs stay reviewable.abienkowski commentedon Oct 7, 2026
One more item, found in the #53 review: the audit
urifield differs across implementations (pre-existing). Go logsr.RequestURI(raw, with the query), Rust logspath(no query), and TS logsreq.url(raw, with the query). After #53, Go'sextra.pathis the escaped routing path, so Go's audit line carries both forms. Proposal: log the raw request target (path + query) asuriin all three, and pin it with a shared audit test.abienkowski commentedon Oct 7, 2026
From the #54 final review (non-blocking): in
release.yml's version step, the|| trueon the latest-tag search also hides a realgit tagfailure. That would fall through to the no-tag path and could cut a strayv0.0.1. It is unlikely withfetch-depth: 0. The tag-selection pipeline (strict filter, version sort,--merged HEAD) also has no automated test; only a one-off simulation covered it.abienkowski commentedon Oct 8, 2026
From the #49 review: ExecGate can never fire, and the README overstates it. All three ExecGates check the substring
/exec(go/internal/middleware/exec.go:14,rs/src/middleware.rs:84,ts/src/middleware.ts:54). The middleware chain runs only onPOST /containers/create, so the gate never sees an exec path; the router denies exec first (segment-exact since #49). The README middleware row ("DeniesPOST /containers/*/execandPOST /exec/*/start") describes something the gate never does. Options: (a) drop the gate; (b) keep it as defence in depth and reword the README row; (c) make it segment-exact like the router, so a future chain caller cannot reintroduce theexec-runnerfalse positive. If the gate stays, (c) matters.abienkowski commentedon Oct 8, 2026
From the #49 final review:
spec/docker_socket_policy.qntendpointsTable(~:181, and the literal list at ~:453) still lists onlyPOST /containers/:name/exec. After #49, exec is denied for every method on/containers/<name>/execand on the whole/exec/*namespace. The table is documentation only (a self-tautology, see spec/README Modeling Notes), so the gap is cosmetic. Update it together with the ExecGate item above.abienkowski commentedon Oct 8, 2026
From the #49 review: double slash before
execskips the exec check in all three routers.GET //exec/<id>/json(and/v1.45//exec/...) has an empty first segment, soisExecPathdoes not match, and the GET passthrough forwards it. Pre-#49 TS (includes("/exec")) denied it, so TS regressed slightly. No data leaks. A live daemon (29.4.0, with a real running exec instance) answers301with an empty body andLocation: /exec/<id>/jsonfor every double-slash form, and a client that follows the redirect re-enters the proxy on the clean path and is denied. Non-GET methods hit the default deny. Proposed shared fix for all three routers plus the Quint model: deny or normalise paths with empty interior segments. This is the same family as the #48 empty-segment rule.chore: call the repo prettier directly; document fmt targets and hooks (
abienkowski commentedon Oct 9, 2026
From the PR #68 review, deferred (not blocking #68):
ts/Dockerfile:26copies the build stage'snode_modulesinto the runtime image, so typescript,@types/nodeand now prettier (~8 MB) ship in a security-sensitive image. Proposal:RUN npm prune --omit=devafternpx tscin the build stage, then verify the reproducible build and the integration suites.rs/rustfmt.tomlhas nostyle_edition. A future edition bump to 2024 would change import ordering and reformat the crate. Consider pinningstyle_edition = "2021"(check that Rust 1.85's rustfmt accepts it) when the edition changes.chore: enforce formatting with lint checks and git hooks (#55) (#68)
test(release): cover a latest-tag lookup with an unresolvable ref (#55)
fix(release): fail on git errors in the latest-tag lookup and test it (…
test: pin the TypeScript absolute-form denial; split the empty-segmen…
fix: deny empty interior path segments; allow HEAD on named container…
fix: log the full request target in Rust audit and unlink the socket …
test: tighten listener, route-table and integration assertions (#55) (#…
docs: state the router model scope and the --readonly gap precisely (#55
refactor: remove the unreachable ExecGate; refresh docs and counts (#55…
abienkowski commentedon Oct 11, 2026
Resolved in separate PRs
ROUTER_SPEC ?=; socket test labels//in the path denied; HEAD on named containers in Rust; specrouter_pre55uri= request target; Rust unlinks the socket when chown/chmod failsFound along the way:
GET http://docker/exec/<id>/json→ 200 with the command line)././exec,/x/../exec), --readonly only blocks container create; lifecycle actions and DELETE are still forwarded #75 (--readonlyonly blocks container create).rs/rustfmt.tomlhas nostyle_edition.