Skip to content

Add Windows warm daemon IPC - #641

Merged
justrach merged 9 commits into
justrach:release/0.2.5828from
nsxdavid:windows-ipc-0.2.5828
Jul 4, 2026
Merged

Add Windows warm daemon IPC#641
justrach merged 9 commits into
justrach:release/0.2.5828from
nsxdavid:windows-ipc-0.2.5828

Conversation

@nsxdavid

Copy link
Copy Markdown
Contributor

Summary

  • Enable the Windows warm-daemon CLI proxy using named-pipe IPC and a Windows daemon lock.
  • Add Windows detached process spawning and bounded subprocess capture needed by daemon startup and local test coverage.
  • Keep the existing POSIX socket/flock path intact; Windows-specific behavior is selected through platform checks.
  • Add a Windows-only warm-daemon test that exercises cold daemon startup followed by a proxied warm query from a Unicode project root.
  • Fix Windows-local test blockers around heap allocation for large watcher queues, environment helpers, rerank trace file access, and POSIX-only tests.

Refs #621.

Validation

  • zig build --global-cache-dir .zig-global-cache test --summary all
    • 23/23 steps succeeded; 858/862 tests passed (4 skipped)
  • zig build --global-cache-dir .zig-global-cache --summary all
    • 3/3 steps succeeded
  • PYTHONIOENCODING=utf-8 python scripts/e2e_mcp_test.py --binary zig-out/bin/codedb.exe --project .
    • 20/20 passed

@nsxdavid
nsxdavid marked this pull request as ready for review June 24, 2026 22:36
@nsxdavid

nsxdavid commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Quick note on the benchmark workflow change in 6541fad:

The first CI run completed the benchmark comparison successfully, but failed afterward when the workflow tried to post bench-report.md as a PR comment from a forked PR token:

Resource not accessible by integration

The compare step still fails the job for real benchmark regressions. This change only makes the final reporting comment best-effort when GitHub does not grant the fork PR token permission to create comments.

The benchmark report is still available in the GitHub Actions run under the bench-results artifact, which includes bench-base.json, bench-head.json, and bench-report.md.

@nsxdavid

Copy link
Copy Markdown
Contributor Author

@justrach this is ready for review now.

I targeted release/0.2.5828 as requested, kept the Windows-specific warm-daemon / named-pipe IPC behavior behind platform checks, and included the platform-gated Windows tests plus local Windows validation results in the PR description. CI is green now as well.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6541fad91c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli_proxy.zig Outdated
Comment thread src/cio.zig

@justrach justrach left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for picking up the named-pipe IPC follow-up @nsxdavid — the MSVCRT argv quoting + round-trip test and the response-size cap are genuinely clean. Before it lands I'd like to tighten the trust model on a couple of the newly-enabled Windows paths. Two main, two minor.

1. Named-pipe squatting → spoofed CLI output + daemon DoS (Medium)
The pipe name is predictable (\\.\pipe\codedb-{Wyhash(0xc0de, root)}-{Wyhash(0xc0de, %USERNAME%)}) and the listener closes + re-creates the instance every loop. A local process can pre-create that name (or win the close/re-create gap):

  • our CreateNamedPipeA then fails and the handler does shutdown.store(true); return; → daemon dies (one-shot DoS);
  • the client opens the name with OPEN_EXISTING, never checks who owns the server end, and writes the reply straight to the terminal (cio.File.stdout().writeAll). A squatter receives the request blob (project path + argv) and returns arbitrary bytes → spoofed output + ANSI-escape injection.

SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION on the client is the right call (stops a malicious server impersonating us) but doesn't authenticate the server to the client. Suggested:

  • Client verifies server: GetNamedPipeServerProcessId → compare that PID's token-user SID to our own; bail on mismatch. (Main fix — defeats squat + instance-hijack regardless of name predictability.)
  • Don't shutdown on a single CreateNamedPipeA failure; log + retry.
  • Close the re-create gap (pre-create the next instance before closing the current, or overlapped accept).

2. runCapture runs unqualified git in the repo cwd → potential Windows binary-planting (High, confirming)
runCaptureWindows now actually runs getGitHead's git rev-parse HEAD with argv[0]="git" and cwd = <repo root>. If Windows resolution searches the cwd (incl. PATHEXT), a git.exe/git.bat planted in a repo executes when codedb tags the snapshot SHA — which is automatic. I'm reproducing this on a Windows box; if it confirms, the fix is to resolve git to an absolute path (PATH search excluding cwd) before spawning. Same care for the other unqualified execs.

3. self-update re-enabled on Windows (Low)runCapture also un-gates download-and-replace; just confirm the Windows update path is TLS + checksum/signature verified.

4. pipe DACL (Low)D:P(A;;GA;;;OW) keys on Owner Rights; for an elevated process the owner can default to Administrators, and there's no integrity label. Consider pinning the ACE to the current user's token SID and adding S:(ML;;NW;;;ME).

Happy to pair on the client-side SID check — that one change closes most of the surface. Thanks again!

@justrach

Copy link
Copy Markdown
Owner

Confirmed F2 (binary-planting → code execution) on a real Windows box — and it's a source-level guarantee, not luck.

Empirical PoC (Windows Server, codedb.exe built from this PR's HEAD with Zig 0.16.0):

  1. plant C:\evilrepo\git.batecho PWNED... > C:\codedb_poc_marker.txt
  2. codedb.exe C:\evilrepo index✓ indexed ✓ index ready 2 files
  3. C:\codedb_poc_marker.txt now exists, contents PWNED_BY_PLANTED_GIT_BAT

So indexing a repo that merely contains a git.bat/git.exe/git.cmd runs it. The trigger is automatic: scanBggetGitHead(root)runCapture({ argv = {"git","rev-parse","HEAD"}, cwd = root }).

Why (Zig 0.16.0 std/Io/Threaded.zig, processSpawnWindows): for an unqualified argv[0], dir_buf is seeded with the child cwd (the repo root) and searched before PATH, wildcard + PATHEXT-expanded, with .bat/.cmd routed through cmd.exe:

// "The cwd provided by options is in effect when choosing the executable path to match POSIX semantics."
const cwd_path_w = ... else if (cwd_w) |cwd| break :x cwd;          // child cwd = repo root
var dir_buf: std.ArrayList(u16) = .empty;
if (cwd_path_w.len > 0) try dir_buf.appendSlice(arena, cwd_path_w);  // dir_buf seeded with cwd
windowsCreateProcessPathExt(arena, &dir_buf, &app_buf, PATHEXT, ...) // searched FIRST, before PATH

So even on a box with real git installed, the planted copy wins (cwd is first in the search).

Fix: resolve git to an absolute path (a PATH search that excludes cwd) before runCapture, or otherwise keep cwd out of the executable search. Same care for the other unqualified execs (telemetry/nuke). Happy to send a patch.

@nsxdavid

Copy link
Copy Markdown
Contributor Author

@justrach working the identified issues

@nsxdavid
nsxdavid force-pushed the windows-ipc-0.2.5828 branch from 6541fad to 0dc0882 Compare June 26, 2026 19:46
@nsxdavid

Copy link
Copy Markdown
Contributor Author

@justrach I updated this PR with the Windows follow-up fixes for the review feedback.

What changed:

  • The CLI client now verifies the named-pipe server before trusting daemon output. It uses GetNamedPipeServerProcessId, opens the server process token, compares that token-user SID with the current process token-user SID, and falls back to the cold path if verification fails.
  • The pipe listener no longer exits on a single CreateNamedPipe failure. It retries with bounded backoff, and it creates the next pipe instance before closing the connected instance to reduce the handoff window.
  • The pipe security descriptor no longer relies on Owner Rights. It is built from the current user SID, with a medium-integrity no-write-up label when supported.
  • Windows subprocess spawning now resolves bare executable names through PATH while excluding the cwd, empty entries, ., and relative PATH entries. This is in the shared runCapture path rather than patched only at individual call sites. There is also a regression test for a repo-local planted git.bat during getGitHead.
  • The Windows daemon lock path now uses Unicode-safe Windows path handling instead of the ANSI CreateFileA path.
  • codedb nuke now handles Windows cli-daemons before deleting .codedb: it reads the daemon metadata, skips the current process, verifies the target process image matches the current codedb.exe, terminates that daemon, and waits for it to exit before deletion continues.

I intentionally did not change self-update behavior in this PR. That seemed out of scope for the IPC hardening work; if you want that tightened, I think it should be handled as a separate, explicit follow-up.

Validation:

  • Local Windows: zig build --global-cache-dir .zig-global-cache test --summary all passed (23/23 steps, 862/866 tests passed, 4 skipped).
  • GitHub: the rerun bench check passed on this PR.

@justrach

Copy link
Copy Markdown
Owner

@codex can you go thru the pr thanks!

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0dc088214f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli_proxy.zig
Comment thread src/cli_proxy.zig Outdated
@nsxdavid

nsxdavid commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@codex Don't know if I am authorized to command you, but on the off chance that I am, please review the latest changes and responses to your previous review comments

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60e3ea93fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli_proxy.zig
@nsxdavid

nsxdavid commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

@justrach this appears ready for merge from my side. The latest head is \cbd76d3, the benchmark check is passing, and the addressed review threads have been replied to and resolved.

Resolves the commands.zig conflict between justrach#645 (.codedbrc disable gating
in runMcp) and this branch's data_dir threading for the Windows pipe
metadata: the cliDaemonListen spawn keeps the new data_dir argument and
stays inside the !project_disabled block, alongside spawnWarmup.
@justrach

justrach commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Heads-up @nsxdavid: three fixes landed on release/0.2.5828 since you opened this (#644, #643, #645), and #645's .codedbrc disable gating in runMcp conflicted with this branch's data_dir threading for the pipe metadata. Since maintainer edits are enabled I resolved it with a merge commit on your branch (fe5ec97 — your history untouched): the cliDaemonListen spawn keeps your new data_dir argument and sits inside the new !project_disabled block.

Validation on the merged tree (macOS): zig build test 23/23 steps, 895/899 passed / 4 skipped (your platform-gated Windows tests), e2e_mcp_test.py 20/20. A full zig build test run on a real Windows VM is in flight — I'll post results here.

…5828

justrach#645 was reverted upstream (37aa83e) — maintainer call, auto-indexing
stays core behavior. commands.zig resolves back to this branch's
original runMcp (data_dir threading, no project_disabled gating).
@justrach

justrach commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Correction to my last note: #645 (.codedbrc disable) was reverted upstream (maintainer call — auto-indexing stays core behavior), so I pushed one more merge commit (a48678d) bringing your branch in line: commands.zig is back to exactly your original runMcp (data_dir threading, no gating). Net effect: your branch = your work + the #642 root-policy fixes, nothing else.

Re-validated after the fixup: zig build test 23/23 steps, 868/872 passed / 4 platform-gated skips, macOS e2e unaffected. Windows-VM validation of the Windows code paths is still running — results shortly.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8dc646f705

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/nuke.zig
Comment on lines +144 to +148
const metadata_path = std.fmt.allocPrint(allocator, "{s}/{s}/cli-daemon.pipe", .{ projects_dir, entry.name }) catch continue;
defer allocator.free(metadata_path);
const metadata = std.Io.Dir.cwd().readFileAlloc(io, metadata_path, allocator, .limited(1024)) catch continue;
defer allocator.free(metadata);
const pid = parseWindowsDaemonPid(metadata) orelse continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reap all Windows daemons before deleting data

When multiple warm daemons exist for the same project, e.g. a serve and an mcp process both running with retry=true, they share this single cli-daemon.pipe record and the last publisher is the only PID this loop sees. nuke then kills just that process and immediately proceeds to deleteTree, while the older daemon can still be alive holding project data files or mmaps open on Windows, so the removal can still fail or leave a daemon behind. Consider repeating the metadata scan/kill until no owner republishes, or tracking all matching codedb warm-daemon PIDs.

Useful? React with 👍 / 👎.

@justrach

justrach commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Full validation is done — merging. Summary for the record:

Windows (real VM, clean environment — Daytona windows-medium, 2 vCPU/8GB, SYSTEM account):

  • zig build test --summary all at this branch's final head (8dc646f): 23/23 steps, 868/872 passed, 4 skipped (the platform-gated Windows tests run here; the skips are the non-applicable gates)
  • zig build --summary all: 3/3
  • One note for other CI-from-scratch environments: five test_mcp cases (buildCliForHelpTests users) spawn zig as a subprocess, so zig must be on PATH — invoking the compiler by absolute path alone isn't enough. Not a code issue.

Non-Windows regression review (independent deep review + local runs):

  • Full suite + e2e_mcp_test.py 20/20 on macOS at head and after both integration merges; base-vs-head test parity verified
  • Pipe security looks genuinely solid: 128-bit random pipe names, DACL restricted to the current user SID, PIPE_REJECT_REMOTE_CLIENTS, and client-side verification of the server pid + SID — squatting and cross-user connects are both closed off
  • One heads-up worth knowing you shipped: the searchSymbols rewrite in explore.zig makes results deterministically ordered (nice), at the cost of scanning all candidates per query — we may follow up with a bounded-insert variant, no action needed from you
  • The 16 MiB proxy response cap now also applies to POSIX (was uncapped): oversized warm responses fall back cold — reasonable hardening, noting it as a behavior change

Thanks @nsxdavid — this is quality work, and it ships in 0.2.5828.

@justrach
justrach merged commit 1183754 into justrach:release/0.2.5828 Jul 4, 2026
1 check passed
justrach added a commit that referenced this pull request Jul 4, 2026
)

appendOne kept its K-bounded candidate buffer sorted by re-running
std.mem.sort on every accepted insert — O(K log K) compares per insert,
plus a redundant final sort of the already-sorted list. The list is
sorted at all times, so place each accepted candidate by binary search +
ArrayList.insert (O(log K) compares + O(K) moves) and drop both sorts.

The comparator also gains line_start as a final tiebreak: same-name
symbols in the same file compared equal, so their relative order still
fell out of hash-map iteration — the residual nondeterminism the
deterministic-ordering rewrite (#641) meant to eliminate.

Gated bench (same-session A/B, quiet-machine controlled via stash test):
codedb_symbol 23.1us -> 19.0us (-18%); payloads byte-identical across
exact/prefix/glob/fuzzy/kind/many-match query shapes; all other tools
unchanged. zig build test 23/23 steps, 869/873 (4 platform skips).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
justrach added a commit that referenced this pull request Jul 4, 2026
…#649)

appendOne kept its K-bounded candidate buffer sorted by re-sorting the
whole buffer on every accepted insert (O(K log K) compares per insert,
plus a redundant final sort). The buffer is always sorted, so placement
is a binary search + insert.

The comparator also gains line_start as the final tiebreak: same-name
same-file symbols compared equal, so their relative order fell out of
hash-map iteration order — the residual nondeterminism the
deterministic-ordering rewrite (#641) meant to eliminate. With the key
ending in (path, line) and same-(path, line) deduped, ties are
impossible and the sorted-insert invariant is total.

A/B on this repo (exact/prefix/pattern/fuzzy/kind × max_results 200):
payload bytes identical on every query; warm prefix query 3.1ms -> 896µs,
name query 1.2ms -> 931µs (embedded dispatch timings). Gated bench:
codedb_symbol +0.23% on the 21-file corpus (exact-name query barely
exercises the insert path — the win scales with match count).

zig build test: 23/23 steps, 869/873 (4 platform skips), incl. a new
ordering test. e2e_mcp_test.py: 20/20.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants