Skip to content

fix: red-team bug-fix sweep (~50 bugs, 49 new tests)#5

Merged
o1x3 merged 17 commits into
mainfrom
fix/red-team-bugfixes
Jun 25, 2026
Merged

fix: red-team bug-fix sweep (~50 bugs, 49 new tests)#5
o1x3 merged 17 commits into
mainfrom
fix/red-team-bugfixes

Conversation

@o1x3

@o1x3 o1x3 commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Red-team bug-fix sweep

A systematic, multi-pass adversarial audit of dcon. Each finding was verified against the code (adversarial second-opinion verification, not taken on faith), fixed, and locked in with a reproducing unit test. Nine audit passes were run over the whole tree; the confirmed-bug count converged pass over pass — 26 → 10 → 5 → 5 → 3 → 1 → 4 → 8 → 0 — with a final full fresh-eyes sweep finding nothing.

~50 bugs fixed, 49 new reproducing tests (187 total). go test -race ./..., go vet ./..., and gofmt are all clean. Non-TTY byte-identical render contract preserved.

Security / isolation

  • machine label-spoof (confused deputy): machine rm/stop/shell NAME resolved by the attacker-controllable dcon.machine.name label, so a non-prefixed dcon run --label dcon.machine=1 --label dcon.machine.name=web container could be acted on. Now resolves strictly by the prefixed backend id + verified label.
  • warm-pool Claim double-hand-out: on a state-write failure Claim returned ok with the member still listed on disk, so two runs could exec into one microVM. Now treated as a miss (cold-run fallback).
  • warm-pool ReapStale TOCTOU: reaped a VM a concurrent run had just claimed. Removal is now atomic under the state lock.

Crash / data-corruption

  • run --mount …,tmpfs-size (no value) panicked (slice OOB) — now bounds-safe.
  • --filter comma-corruption on ps/images/volume/network (StringSlice split label values) → StringArray.
  • compose long-form ports:/volumes: (sequence-of-maps) failed to unmarshal, breaking the whole file → PortList/VolumeList.
  • machine rm --force defaulted true (always force-deleted a running machine) → default false.

compose engine correctness

  • interpolation: only ${VAR:-default} worked; ${VAR:?}/${VAR:+}/-/+/:= silently produced empty even for set vars, and $$ (literal $) was deleted. Full bash/compose parameter-expansion now implemented; required-but-unset ${VAR:?} aborts load.
  • named volumes mounted the bare key, not the project-scoped <project>_name that up actually creates (container got the wrong volume; down -v removed the wrong one).
  • build+image services: --build tagged the derived name but ran image: — the freshly built image was never used.
  • run --rm=false stripped a literal --rm from the user's command; entrypoint override / command override dropped or kept the wrong post-image tokens; one-off run containers were labeled oneoff=False and collided with service replicas in exec/ps/down.
  • exec -T … < file dropped --interactive (redirected stdin lost); compose run never forwarded --tty/--interactive; kill --signal / stop|restart --timeout were dropped.
  • up --scale SERVICE=0 ran 1; images [SERVICE] ignored the filter; ls/config -q, ls --all were dead flags; multiple -f files were silently dropped (now warned); -f/-p shorthand broke when a root flag preceded compose.

CLI parity

  • top web -ef (dashed ps options) no longer errors; ps -n 0 shows none; cp ./a:b.txt … no longer misread as a container ref; history truncates by runes (valid UTF-8); restart --signal honored; inspect <container> <image> (mixed) works; build --output …,name= preserved as a tag; images registry-port/digest/bad-pattern filters fixed; system prune propagates errors; volume create --name honored; stats | cat no longer injects ANSI.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • compose up supports --pull always; build output specs are translated more reliably (including name→tag behavior).
  • Bug Fixes
    • Improved flag forwarding and filtering across compose, ps, images, volume/network ls (comma-safe labels, status= behavior, ancestor= matching, correct -n/--last).
    • Fixed copy/container detection, CPU parsing edge cases, created-by truncation, stats screen clearing, and compose log follow/shutdown timeout handling.
    • Safer machine/volume handling (e.g., machine rm force default, stricter reserved namespace checks, volume create name conflicts).
  • Chores
    • Expanded regression test coverage and strengthened template/table rendering behavior.

o1x3 added 15 commits June 25, 2026 10:17
1. resolveMachine matched a container when the user-controllable
   dcon.machine.name label equalled the requested name. Because
   `dcon run` forwards arbitrary --label to the backend, an ordinary
   container created with --label dcon.machine=1 --label
   dcon.machine.name=web (and any non-prefixed name) would be resolved as
   machine `web`, turning `machine rm/stop/shell web` into a confused
   deputy against an arbitrary container and defeating the dcon-machine-
   prefix namespace. Resolve strictly by the prefixed backend id plus the
   verified dcon.machine label via a new pure matchMachine helper; a
   genuine machine always has id == dcon-machine-<name>, so no real
   capability is lost.

2. `machine rm` had --force default to true, so a bare `rm` always
   force-deleted a running machine (irreversible FS loss) and -f was a
   no-op. Default to false to mirror `docker rm`: a bare rm of a running
   machine fails and tells the user to pass -f.

Both bugs are covered by reproducing unit tests.
ports: and volumes: were plain []string, so the Compose long (mapping)
form — e.g. 'ports: [{target: 80, published: "8080"}]' or
'volumes: [{type: bind, source: ./d, target: /d}]' — failed to unmarshal
and hard-errored the entire compose file (every up/down/ps/config).
This is valid, common syntax (what 'docker compose config' emits).

Add PortList/VolumeList named types that accept both the short string and
long mapping forms, flattening the long form to the same
[host_ip:][published:]target[/proto] and [source:]target[:ro] strings the
translator already emits. yaml scalar .Value is read directly so numeric
ports (- 9000, target: 80) don't trip an int->string decode.

Also drop blank/keyless environment & labels list entries ('' or
'=value') in MapList, which previously injected a malformed empty-key
'--env =' argument.

Both covered by reproducing tests.
…ed one

ReapStale enumerated members with List(), then for each stale one called
forget() and DestroyAsync() as separate steps. A concurrent Claim could
pop a member in that window; the reaper would then DestroyAsync the very
VM the live run was about to exec into. (It self-healed via the cold-path
fallback, but wasted the warm slot.)

Pop stale members inside the state lock via a pure partitionStale helper,
then destroy them after releasing it. A concurrent Claim and a reap can
now never both own the same member.
…ef filters

- run: a valueless '--mount type=tmpfs,...,tmpfs-size' (no =value) sliced
  past the string end and panicked the process; pass such fields through.
- ps/images/volume/network: --filter was StringSlice, which comma-splits a
  single value, so 'label=team=a,b' became two bogus filters and matched
  the wrong set. Use StringArray (the project's comma-bearing-flag rule).
- ps: 'ps -n 0' listed every running container instead of none; trimLast
  now distinguishes the unset sentinel (-1) from an explicit 0.
- images: a registry-port ref (registry:5000/img) was misread as tag
  ':latest' and hid all other tags; a digest ref (img@sha256:..) compared
  the digest against the tag column and always returned empty. Parse via
  imageRefFilter and match digests against the digest column.
- images: a malformed reference= glob silently hid every image; validate
  the pattern up front and error like docker.

All covered by reproducing tests.
…ness

compose:
- exec gated --interactive on stdin being a TTY, so 'compose exec -T db
  psql < dump.sql' dropped the redirected stdin. Forward --interactive on
  the flag value alone (TTY handled separately) via a pure composeExecArgs.
- run --rm=false used a global token strip that removed EVERY '--rm',
  including one passed to the in-container command. Pass rm into OneOffArgs
  so the run-level flag is controlled at the source; delete dropFlag.
- up/create's '--detach' strip had the same hazard for a service command
  containing a literal --detach; add a positional CreateArgs builder.
- warn (don't silently drop) when multiple -f/--file files are given,
  since dcon does not merge them like docker.

cli:
- top: pass dashed ps options through (SetInterspersed false) so
  'top web -ef' no longer dies on an unknown-flag parse error.
- cp: a local path with a colon (./my:file.txt) was misread as a
  CONTAINER:PATH ref; classify like docker's splitCpArg.
- history: truncate CREATED BY by runes, not bytes, so multibyte UTF-8
  isn't split into an invalid sequence.
- system df --verbose / prune --filter now warn instead of silently
  ignoring; system prune propagates backend errors instead of always
  exiting 0.

All covered by reproducing tests.
- build: '--output type=docker|image,name=X' with no -t silently dropped
  the name, yielding an untagged image. A dest-less type=docker/image is
  buildx's local-store load (the long form of --load) — the backend
  default — so omit --output and carry name= through as --tag. With dest
  it stays a file export (type=oci). translateOutput now returns (output,
  tag, err).
- restart: the --signal flag was defined but never used; forward it to the
  stop phase (the backend stop accepts --signal), matching docker.
- inspect: a mixed 'inspect <container> <image>' failed because each
  namespace was tried as one batch. Fall back to per-id resolution and
  merge the JSON arrays so both print, like docker.

All covered by reproducing tests.
…ly used

A service with both build: and image: built an image tagged with the
derived project name (project-service:latest) via BuildArgs, but the
container was run as svc.Image (imageRef). So 'compose up --build' (and
'compose build') produced an image the run never referenced — the run used
a stale/pulled svc.Image instead of the freshly built one.

Tag the built image with imageRef (svc.Image when set, else the derived
name), matching what RunArgs/OneOffArgs run. This also makes
'down --rmi local' consistent: a service with a custom image: keeps its
tag (removed only by --rmi all), while build-only services still tag and
remove the derived name.

Covered by a reproducing test.
compose:
- run now forwards --tty/--interactive (defaults: keep stdin open, allocate
  a TTY unless -T or no terminal); previously it read neither so
  'compose run web bash' got no PTY. (composeRunPtyFlags)
- OneOffArgs: overriding --entrypoint on a multi-token service entrypoint
  no longer leaks the old entrypoint's trailing args onto the new one.
- one-off run containers are labeled oneoff=True and excluded by the
  service-replica resolvers, so 'compose exec/ps' no longer confuses a
  surviving 'run --rm=false' container with replica #1.
- up --scale SERVICE=0 now runs zero replicas (explicit 0 was treated as
  unset and clamped to 1). (effectiveReplicas)
- long-form port host_ip without published emits host_ip::target, not the
  malformed host_ip:target (which parsed as host_port:container).
- bare environment/build-arg keys (- FOO / FOO:) inherit the host value
  (compose passthrough) instead of being forced to FOO="". (EnvMap)
- correct the inaccurate Levels() cycle doc comment.

cli:
- port expands a published range (PublishPort.Count>1) to one line per
  port and resolves per-port filters into it. (portMappingLines)

dockerfmt:
- table header derivation handles function/pipeline-prefixed actions
  ({{upper .Name}}) instead of leaking the raw template into the header.

pool:
- Claim treats a state-write failure as a miss instead of returning ok on
  an unpersisted pop, preventing the same warm VM being handed out twice.

All covered by reproducing tests.
…ssions)

- compose long-form volume read_only: parse as a boolean (ParseBool), so
  'read_only: True'/'TRUE'/'1' produce a :ro mount instead of silently
  mounting read-write. (regression in the pass-1 long-form volume support)
- OneOffArgs: a command override no longer drops the service entrypoint's
  extra tokens when --entrypoint is NOT overridden, so 'compose run web
  shell' on entrypoint [python,-m,flask] runs 'python -m flask shell', not
  'python shell'. Post-image tokens are now split into entrypoint-extras
  (kept unless the entrypoint is replaced) and command (replaced by the
  override). (regression in the pass-2 entrypoint fix)
- compose ls -q now prints only project names (the flag was ignored).
- volume create --name is honored instead of silently creating a
  random-named volume; supplying both --name and a positional errors.
- dockerfmt table header derivation strips string literals before scanning
  for the field, so a dotted token inside a printf format
  ({{.Name | printf "%s.txt"}}) no longer becomes the header column.

Reproducing tests for all except 'compose ls -q' (backend-coupled glue).
- compose: $$ is compose's escape for a literal $; Load() was deleting it
  (echo $$HOME -> echo HOME). Collapse $$ to a single $ so literal-dollar
  values (shell vars, cron, hashes) survive interpolation.
- compose images now honors the [SERVICE...] positional filter instead of
  always listing every service's image.
- compose ls defaults to projects with a running container; -a/--all now
  actually includes fully-stopped projects (the flag was dead).
- compose config -q/--quiet now validates silently (prints nothing)
  instead of dumping the full rendered YAML.
- stats: the live-table clear-screen escape is gated on a real terminal,
  so 'dcon stats | cat' no longer injects raw ANSI into piped output.

$$-escape and config -q covered by reproducing tests; the others are
backend-coupled paths (per the repo's manual-validation convention).
… flags

- interpolation: Load() only honored ${VAR:-default}; every other operator
  (:?, :+, -, +, :=) silently produced empty — even for SET variables, so
  'image: app:${TAG:?required}' with TAG=v2 rendered 'app:'. Implement the
  bash/compose parameter-expansion family in a new expandVar (-/:- default,
  +/:+ alternate, ?/:? required-with-error, =/:= default); a required-but-
  unset ${VAR:?} now aborts Load with a clear error.
- named volumes: a service mount of a declared top-level volume used the bare
  key (e.g. 'data:/var/lib') while ensureVolumes created '<project>_data', so
  the container got a different (auto-created) volume than declared and
  'down -v' removed the wrong one. resolveVolume now rewrites a declared named
  volume to its VolumeName (project-scoped / explicit name:), mirroring the
  network resolution.
- compose kill --signal and stop/restart --timeout were registered but
  dropped; lifecycleOnProject now forwards them (the backend kill/stop accept
  --signal/--time), via composeKillArgs/composeStopArgs.

All covered by reproducing tests (expandVar table, required-var Load error,
resolveVolume named volume, kill/stop arg builders).
…`compose`

rewriteComposeGlobalShorthands early-returned unless args[0]=="compose", so
a root persistent flag before the subcommand (`dcon -D compose -f x.yml up`,
`dcon --host x compose -p proj down`) skipped the rewrite and cobra then
hard-failed with 'unknown shorthand flag: f', diverging from docker which
accepts global flags before `compose`.

Add composeIndex to locate the `compose` token past any root flags (skipping
the value of separated value-flags like --host/--context/--tls*), and rewrite
from there. Covered by new test cases.
…s, doctor builder

- parseCPUs accepted inf/NaN (ParseFloat parses them), emitting --cpus
  9223372036854775807 (Inf) or --cpus 0 (NaN) to the backend; reject
  non-finite values like docker does. Shared by run/create/machine create.
- --format 'table {{.A}}\t{{.B}}' (and the plain template branch) rendered
  a literal backslash-t and never aligned columns; unescape \t/\n to real
  bytes like the docker CLI (this is the documented recipe in SECONDARY.md).
- build --progress quiet was forwarded verbatim and rejected by the backend
  (accepts auto|plain|tty); remap quiet->plain (alongside rawjson).
- doctor's image-builder check used substring "running", which also matches
  "not running"; exclude the negative form via builderRunning.

All covered by reproducing tests.
…s 8)

Recurring-class sweep (ignored flags, substring matches):
- compose up now honors --no-log-prefix (foreground), --timeout (stop grace),
  and --pull always (force-refresh each service image before start). These were
  registered with real-behavior help but never read. (formatLogLine extracted
  for the prefix; followAndWait threads noPrefix + timeout.)
- docker info reported the backend as 'running' when stopped: it used a
  substring match ("running" is inside "not running"). Use parseSystemStatus
  exact-match, like doctor.
- ps --filter status=exited/created/dead returned nothing without -a (default
  fetch was running-only); a status= filter now forces the all-states fetch.
- ps --filter ancestor=alpine matched superstrings like 'myalpine' (substring);
  match repo / repo:tag / full ref via ancestorMatches.
- images REPO filter was built from the un-normalized ref, so
  'images docker.io/library/alpine' returned empty; normalize via ShortImage.
- compose port --protocol UDP was compared case-sensitively to the backend's
  lowercase proto.

Pure helpers (hasStatusFilter/ancestorMatches/imageRefFilter/formatLogLine)
covered by reproducing tests.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The PR updates CLI parsing and command wiring, revises Compose model and translation behavior, changes Docker-format rendering, and adjusts pool stale-member reaping and claim handling. Several tests were added or updated for the affected paths.

Changes

CLI, Compose, and Runtime Changes

Layer / File(s) Summary
Shared CLI parsing and filters
cmd/compose.go, cmd/filters_test.go, cmd/images.go, cmd/misc.go, cmd/network.go, cmd/parity_test.go, cmd/ps.go, cmd/volume.go, cmd/cli_fixes_test.go
Compose shorthand rewriting, comma-safe filters, ancestor/status/last matching, mixed inspect merging, and container-reference detection are updated together.
Output and argument helpers
cmd/build.go, cmd/history.go, cmd/run.go, cmd/doctor.go, cmd/stats.go, cmd/translate_test.go, cmd/cli_fixes_test.go
Docker output translation, created-by formatting, CPU and mount parsing, builder status detection, and terminal-gated stats output change with their tests.
System, volume, and machine commands
cmd/system.go, cmd/volume.go, cmd/machine.go, cmd/machine_test.go, CLAUDE.md, cmd/cli_fixes_test.go
System prune and info handling, volume name resolution and filter parsing, machine matching and delete defaults, and matching docs/tests change together.

Compose Command Flow

Layer / File(s) Summary
Compose up and lifecycle flow
cmd/compose.go, cmd/lifecycle.go, cmd/cli_fixes_test.go
compose up pulls images, computes replicas, follows logs, and forwards stop and kill flags through lifecycle helpers.
Compose subcommands and selection
cmd/compose.go, cmd/compose_helpers_test.go
Compose config, ls, create, exec, run, images, port, and service-container lookup use new argv builders, PTY flags, quiet mode, and one-off filtering.

Compose Model and Translation

Layer / File(s) Summary
Model decoding and interpolation
internal/compose/model.go, internal/compose/model_test.go
Compose environment, port, and volume YAML decoding now flattens short and long forms, and interpolation supports compose-style parameter expansion with required-variable errors.
Run and build arg translation
internal/compose/translate.go, internal/compose/translate_test.go, internal/compose/model_test.go
runArgs, OneOffArgs, resolveVolume, and BuildArgs change how one-off labels, --rm, command tokens, named volumes, and build tags are emitted.

Docker Format Rendering

Layer / File(s) Summary
Template unescaping and headers
internal/dockerfmt/render.go, internal/dockerfmt/render_test.go
Escaped tabs and newlines are unescaped before template execution, and table headers are derived from the last field reference inside each action.

Pool Stale Handling

Layer / File(s) Summary
Reap and claim behavior
internal/pool/pool.go, internal/pool/pool_test.go
ReapStale partitions members under lock before destroying stale VMs, partitionStale splits members by cutoff, and Claim returns a miss when persistence fails.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through flags and tags so bright,
And tucked the compose logs in tidy sight.
I nibbled tabs into rows so neat,
Then burrowed stale pools where the quiet winds meet,
Hop-hop—code glows, and tests all complete!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the PR’s broad bug-fix sweep and added regression tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/red-team-bugfixes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 markdownlint-cli2 (0.22.1)
CLAUDE.md

markdownlint-cli2 v0.22.1 (markdownlint v0.40.0)
Error: Unable to use configuration file '/coderabbit-0.markdownlint-cli2.jsonc'; ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at throwForConfigurationFile (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:48:9)
at readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:169:5)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
[cause]: Error: ENOENT: no such file or directory, open '/coderabbit-0.markdownlint-cli2.jsonc'
at async open (node:internal/fs/promises:640:25)
at async Object.readFile (node:internal/fs/promises:1287:14)
at async readOptionsOrConfig (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:141:17)
at async main (file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2.mjs:927:21)
at async file:///usr/local/lib/node_modules/markdownlint-cli2/markdownlint-cli2-bin.mjs:14:22 {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/coderabbit-0.markdownlint-cli2.jsonc'
}
}


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (2)
cmd/compose_helpers_test.go (1)

97-124: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a failing-config case for config -q.

This test proves quiet mode suppresses stdout for valid input, but it would still pass if -q returned before validation. Add an invalid compose file assertion so the “still validate” contract is covered.

Proposed test coverage addition
 	if out := run(nil); !strings.Contains(out, "nginx") {
 		t.Errorf("config (no -q) should render the config; got %q", out)
 	}
+	if err := os.WriteFile(filepath.Join(dir, "compose.yaml"),
+		[]byte("services:\n  web:\n    image: [\n"), 0o644); err != nil {
+		t.Fatal(err)
+	}
+	cmd := composeConfig()
+	cmd.SetArgs([]string{"-q"})
+	cmd.SilenceUsage, cmd.SilenceErrors = true, true
+	if err := cmd.Execute(); err == nil {
+		t.Fatal("config -q should still validate and fail on invalid compose.yaml")
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/compose_helpers_test.go` around lines 97 - 124, `TestComposeConfigQuiet`
only checks that `composeConfig` suppresses output on valid input, but not that
`-q` still validates the compose file. Extend this test with a failing-compose
case using the same `composeConfig()`/`cmd.Execute()` path and assert an invalid
`compose.yaml` returns an error even when `-q` is set, so the quiet flag cannot
bypass validation.
cmd/translate_test.go (1)

167-169: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the mount value, not just the flag.

The test says the valueless key must pass through untouched, but it would still pass if the implementation dropped or rewrote the mount payload.

Proposed test tightening
-		if !contains(got, "--mount") {
-			t.Errorf("%q: expected a --mount arg; got %v", spec, got)
+		if !containsPair(got, "--mount", spec) {
+			t.Errorf("%q: expected unchanged --mount payload; got %v", spec, got)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/translate_test.go` around lines 167 - 169, The test in translate_test
should verify the full mount argument, not only the presence of the "--mount"
flag. Update the assertion around the spec handling in the translate test to
check the exact mount payload/value produced by the translate logic, so it fails
if the implementation drops, rewrites, or alters the argument while still
keeping the same flag name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/compose.go`:
- Around line 439-442: Short-circuit the foreground compose up flow when no
containers were started, since effectiveReplicas() can now return 0 and
followAndWait() will otherwise hang on the signal channel. In the command path
that currently unconditionally calls followAndWait(p, started, noPrefix,
timeoutChanged, timeout), add an early return when started is empty so --scale
svc=0 exits cleanly. Apply the same guard in the other foreground up path that
also delegates to followAndWait, keeping the behavior consistent across both
call sites.

In `@cmd/machine.go`:
- Around line 120-127: `dcon run` is allowing users to create containers that
match the machine namespace, which can be picked up by `matchMachine` and
interfere with `machine stop`. Update the name/label validation in `cmd/run.go`
to import and use `internal/machine`’s `ValidateName` so machine-style names are
rejected, and also block any `dcon.machine` label from being set on run-created
containers. Ensure the checks happen where the container name and labels are
assembled before the container is created.

In `@cmd/misc.go`:
- Around line 186-190: The cpIsContainerRef helper is still misclassifying
native Windows drive-letter paths as container refs because it only excludes
absolute and dot-prefixed paths before checking for a colon. Update
cpIsContainerRef to explicitly recognize Windows local paths such as
drive-letter formats before the strings.IndexByte(p, ':') check, and add a
regression case in TestCpIsContainerRef for C:\tmp\file and C:/tmp/file to
ensure they are treated as local paths.

In `@cmd/ps.go`:
- Around line 197-200: Normalize the ancestor filter value in ancestorMatches
before comparing, since only the reference is currently passed through
dockerfmt.ShortImage and fully qualified ancestor values like
docker.io/library/alpine can miss matches. Update ancestorMatches to derive a
shortened form for val as well, then compare reference, short, repo, and
repo:tag against both normalized forms so ancestor filters work for stored
shortened image refs.

In `@cmd/volume.go`:
- Around line 27-37: The resolveVolumeName helper currently falls through to
randomName() when --name was explicitly provided but empty, so update the
flag/args handling in resolveVolumeName to treat nameChanged as an explicit
input even when flagName is empty and return an error instead of generating a
volume name. Keep the existing conflict check for positional args vs --name, and
add a regression test covering resolveVolumeName("", true, nil) to assert that
an empty explicit name is rejected rather than producing an anonymous volume.

In `@internal/compose/model_test.go`:
- Around line 265-293: The test currently only exercises Service.Environment via
EnvMap, so it no longer protects the MapList.UnmarshalYAML empty-key behavior.
Update TestMapListSkipsEmptyKeys to include a labels: case (or rename it to
reflect EnvMap coverage) and assert that blank or keyless entries are dropped
through MapList so the regression remains covered.
- Around line 300-311: Harden the unset-variable tests in the compose model
tests by making the host environment deterministic and verifying the fixture
file write. In TestEnvBareKeyHostPassthrough and the related unset-var cases
(including the other referenced tests), explicitly clear or isolate
DCON_UNSET_XYZ, UNSETV, and DCON_REQ_UNSET before calling Load so assertions
don’t depend on the runner’s environment. Also check the result of os.WriteFile
when creating the compose.yaml fixture so the test fails immediately if the file
was not written, using the existing test helpers and Load call sites as anchors.

In `@internal/compose/model.go`:
- Around line 381-392: The flattenLongVolume helper is dropping the mount type,
so a long-form Compose mount with type set to tmpfs gets converted into a normal
volume instead of preserving tmpfs behavior. Update flattenLongVolume and the
surrounding mount conversion logic in internal/compose/model.go to either route
type: tmpfs through the existing tmpfs handling path or explicitly reject it,
and make sure the logic still uses source, target, and read_only correctly for
non-tmpfs mounts.
- Around line 510-524: The interpolation in model loading is happening on the
raw YAML string, so required-variable errors from os.Expand can be triggered by
comments or single-quoted literals that should stay literal. Update the
Load/expansion flow around expandVar and os.Expand in model.go to interpolate
only parsed scalar nodes (or otherwise skip comments and single-quoted values)
before propagating interpErr, so required vars are enforced only where Compose
actually treats them as expandable.

In `@internal/compose/translate.go`:
- Around line 353-355: The volume translation in translate.go is incorrectly
prefixing external named volumes via Project.VolumeName, causing mount name
collisions. Update the logic around the p.Volumes lookup and the VolumeName
helper so that when a VolumeSpec is marked external, it returns the explicit
spec.Name if present, otherwise the raw volume key unchanged, instead of
applying the project-name prefix. Keep the existing behavior only for
non-external declared volumes and use the VolumeName/Volumes lookup path to
distinguish these cases.

In `@internal/pool/pool_test.go`:
- Around line 15-40: The regression test in pool_test.go currently depends on
directory permissions and skips under root, so make it deterministic for all
users by forcing save() to fail via the temp-file path instead. Update the
Claim/Add setup around dir(), withLock/save(), and the os.Chmod-based failure
simulation so that pool.json.tmp is created as a directory before calling
Claim("alpine"), then restore cleanup and keep the assertion that the member is
still claimable afterward.

In `@internal/pool/pool.go`:
- Around line 241-247: The reaping path in partitionStale/withLock is destroying
VMs before the state change is guaranteed to persist. Update the stale-member
removal flow so the result from withLock is checked and DestroyAsync is only
called after save(s) succeeds and pool.json has been updated; if persistence
fails, do not destroy any VMs. Use the existing symbols withLock,
partitionStale, DestroyAsync, and state/Members to keep the fix localized.

---

Nitpick comments:
In `@cmd/compose_helpers_test.go`:
- Around line 97-124: `TestComposeConfigQuiet` only checks that `composeConfig`
suppresses output on valid input, but not that `-q` still validates the compose
file. Extend this test with a failing-compose case using the same
`composeConfig()`/`cmd.Execute()` path and assert an invalid `compose.yaml`
returns an error even when `-q` is set, so the quiet flag cannot bypass
validation.

In `@cmd/translate_test.go`:
- Around line 167-169: The test in translate_test should verify the full mount
argument, not only the presence of the "--mount" flag. Update the assertion
around the spec handling in the translate test to check the exact mount
payload/value produced by the translate logic, so it fails if the implementation
drops, rewrites, or alters the argument while still keeping the same flag name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a5bdf44-d9af-497c-bb88-9a392bde75a3

📥 Commits

Reviewing files that changed from the base of the PR and between 19519bf and 01af7ee.

📒 Files selected for processing (28)
  • cmd/build.go
  • cmd/cli_fixes_test.go
  • cmd/compose.go
  • cmd/compose_helpers_test.go
  • cmd/doctor.go
  • cmd/filters_test.go
  • cmd/history.go
  • cmd/images.go
  • cmd/lifecycle.go
  • cmd/machine.go
  • cmd/machine_test.go
  • cmd/misc.go
  • cmd/network.go
  • cmd/parity_test.go
  • cmd/ps.go
  • cmd/run.go
  • cmd/stats.go
  • cmd/system.go
  • cmd/translate_test.go
  • cmd/volume.go
  • internal/compose/model.go
  • internal/compose/model_test.go
  • internal/compose/translate.go
  • internal/compose/translate_test.go
  • internal/dockerfmt/render.go
  • internal/dockerfmt/render_test.go
  • internal/pool/pool.go
  • internal/pool/pool_test.go

Comment thread cmd/compose.go
Comment thread cmd/machine.go
Comment thread cmd/misc.go
Comment thread cmd/ps.go Outdated
Comment thread cmd/volume.go
Comment thread internal/compose/model.go
Comment thread internal/compose/model.go
Comment on lines +510 to +524
// Expand ${VAR} / $VAR from the environment, like compose does, supporting
// the bash-style operators compose honors: :-/- (default), :+/+ (alternate),
// :?/? (required, error if missing), :=/= (default). A required-but-missing
// variable is surfaced as a load error.
var interpErr error
expanded := os.Expand(string(data), func(key string) string {
// support ${VAR:-default}
if i := strings.Index(key, ":-"); i >= 0 {
name, def := key[:i], key[i+2:]
if v, ok := os.LookupEnv(name); ok {
return v
}
return def
v, err := expandVar(key)
if err != nil && interpErr == nil {
interpErr = err
}
return os.Getenv(key)
return v
})
if interpErr != nil {
return nil, interpErr
}

@coderabbitai coderabbitai Bot Jun 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Avoid expanding required vars across raw YAML text.

Because os.Expand runs before YAML parsing, ${MISSING:?msg} inside comments or single-quoted literals will now abort Load, even though those locations should remain literal. Interpolate parsed scalar nodes instead, or at least skip comments/single-quoted values before surfacing required-variable errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/compose/model.go` around lines 510 - 524, The interpolation in model
loading is happening on the raw YAML string, so required-variable errors from
os.Expand can be triggered by comments or single-quoted literals that should
stay literal. Update the Load/expansion flow around expandVar and os.Expand in
model.go to interpolate only parsed scalar nodes (or otherwise skip comments and
single-quoted values) before propagating interpErr, so required vars are
enforced only where Compose actually treats them as expandable.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially declining. The single-quote half of this is incorrect for Compose: YAML single-quotes are not shell quotes, and Compose interpolates string values regardless of quoting style ($$ is the only escape) — so '${VAR}' is expected to expand. The genuinely valid part is ${VAR:?} inside a YAML comment wrongly aborting Load, which is a narrow edge case.

The correct general fix (interpolate parsed scalar nodes instead of raw text) is a load-path rewrite that would touch the $$-escape, default-operator, and required-var tests added in this sweep. I'd rather not land that refactor under a bugfix-cleanup pass where the risk/reward is poor; tracking it separately as a follow-up rather than rushing it here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@o1x3, that makes sense — thanks for the correction.

You're right that the single-quoted part of my comment was off: for Compose, YAML quoting does not suppress interpolation, and $$ is the escape that matters. The remaining real issue is the narrower comment-path false positive from doing os.Expand(...) on raw YAML before parsing.

Given the scope here, I agree not forcing a load-path rewrite into this bugfix sweep is the better tradeoff. If you want, I can open a small follow-up issue to track the comment-only interpolation bug separately.

(ᵔᴥᵔ)₊˚🐇


✏️ Learnings added
Learnt from: o1x3
Repo: o1x3/dcon PR: 5
File: internal/compose/model.go:520-534
Timestamp: 2026-06-25T18:04:40.473Z
Learning: In `internal/compose/model.go`, Compose variable interpolation should still apply to YAML single-quoted string values; YAML single quotes are not treated as shell-quoting, and `$$` is the relevant escape mechanism.

Learnt from: o1x3
Repo: o1x3/dcon PR: 5
File: internal/compose/model.go:520-534
Timestamp: 2026-06-25T18:04:40.474Z
Learning: For this PR in repository `o1x3/dcon`, the remaining interpolation concern in `internal/compose/model.go` is specifically that raw-text `os.Expand(...)` can incorrectly enforce `${VAR:?}` inside YAML comments. The maintainer chose to defer a broader parsed-node interpolation refactor to a follow-up change because it would touch the `$$` escape, default-operator, and required-variable test coverage added in this bugfix sweep.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread internal/compose/translate.go
Comment thread internal/pool/pool_test.go Outdated
Comment thread internal/pool/pool.go Outdated
o1x3 added 2 commits June 25, 2026 23:32
Resolve the actionable CodeRabbit findings on the red-team sweep:

- compose up: short-circuit the foreground path when zero containers start
  (`up --scale svc=0` no longer hangs on the signal channel)
- run/create: reject the reserved `dcon-machine-` name prefix and
  `dcon.machine*` labels so `dcon run` can't forge a container that
  `dcon machine stop` would resolve (confused-deputy fix)
- ps --filter ancestor: normalize the filter value too, so a fully
  qualified ancestor matches a shortened stored ref (and vice-versa)
- volume create: an explicit empty --name now errors instead of silently
  creating an anonymous volume
- compose: route long-form `type: tmpfs` volumes to --tmpfs instead of a
  disk-backed anonymous --volume
- compose: external named volumes reference their exact name, never the
  project-prefixed one
- pool: destroy reaped VMs only after the state removal is persisted; a
  failed save() no longer tears down still-claimable members

Tests: machine-namespace rejection, ancestor normalization, empty --name,
tmpfs routing, external volumes, MapList label empty-key drop, plus
deterministic unset-var fixtures and a root-proof pool save-failure case.
Add the internal/ui and internal/machine architecture notes to CLAUDE.md
and update the make build size estimate (~5.3 MB -> ~6.2 MB).
@o1x3

o1x3 commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Addressed in 1365... fix: address CodeRabbit review (pass 9)

Fixed (10):

  • compose up foreground path no longer hangs on --scale svc=0 (zero started ⇒ early return)
  • run/create reject the reserved dcon-machine- name prefix and dcon.machine* labels (machine-namespace confused-deputy)
  • ps --filter ancestor= normalizes the filter value too (fully-qualified ⇄ shortened ref)
  • volume create errors on an explicit empty --name instead of generating an anonymous volume
  • long-form type: tmpfs volumes route to --tmpfs, not a disk-backed --volume
  • external named volumes reference their exact name (no project prefix)
  • pool reap destroys VMs only after the state removal persists
  • tests: MapList label empty-key coverage, deterministic unset-var fixtures, root-proof pool save-failure, config -q still-validates, tightened --mount passthrough assertion

Declined (2) — replies in-thread:

  • cmd/misc.go Windows drive-letter handling: would break single-letter container names (c:/path) on darwin, the only platform dcon runs on.
  • internal/compose/model.go per-node interpolation: single-quote premise doesn't match Compose semantics; the real (comment) edge case needs a load-path rewrite tracked as a separate follow-up, not this cleanup pass.

@o1x3 o1x3 merged commit 557425f into main Jun 25, 2026
2 of 3 checks passed
@o1x3 o1x3 deleted the fix/red-team-bugfixes branch June 25, 2026 18:06
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