Visitar URL original
fix(acq): make the provider-input guard work on windows by mogul · Pull Request #667 · GSA-TTS/agentic-coding-quickstart · GitHub
Skip to content

fix(acq): make the provider-input guard work on windows - #667

Open
mogul wants to merge 7 commits into
mainfrom
fix/windows-provider-input-guard
Open

mogul wants to merge 7 commits into
mainfrom
fix/windows-provider-input-guard

Conversation

@mogul

@mogul mogul commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

acq terminate could not run at all on a Windows host: the provider-input
transaction wrapper aborted before it reached backend teardown, so recorded port
state survived and acq rm / acq stop left orphan tunnels behind. This makes
the guard work on Windows without weakening it, and leaves the POSIX path
byte-for-byte unchanged.

Two independent host gaps caused it:

  1. The path walk used POSIX-only primitives. _acq_provider_input_safe_mkdir
    and _acq_provider_input_safe_dir walk a path with
    os.open(..., O_DIRECTORY | O_NOFOLLOW, dir_fd=...). Native Windows CPython
    has no os.O_DIRECTORY, no os.O_NOFOLLOW, and no dir_fd support, and
    cannot open a directory as a file descriptor at all, so the step raised and the
    transaction aborted.
  2. The process guards used ps -o. _acq_provider_input_guardian_start and
    _acq_provider_input_process_identity ran ps -o ppid= / ps -o lstart=.
    MSYS ps has no -o (it prints ps: unknown option -- o and nothing else),
    so the guardian had no transaction parent and aborted with return 1.

Dependency on #663 (merged)

#663 (the native Windows jq CRLF fix) has
merged, so this PR now targets main directly and no longer depends on another
open PR. This branch carries one commit that duplicates it (1b0f165); it is
identical to the merged change and drops out of the diff against main.

test/bats/112-msb-ports.bats test 13 (LIST mode surfaces create-time + post-hoc ports …) needs that CRLF fix, which lives in #663 and not here:
acq.backends/msb.sh is unchanged by this PR. Test 11 (rm and stop tear down recorded port state …) needs the guard fix in this PR.

Review follow-up: the per-component check no longer compares the resolved name
with the literal component text. It now requires the resolved path to be a
direct child of the vetted parent's resolved path, so a legitimate path that
contains an 8.3 short-name alias (for example PROGRA~1) is accepted, while a
component that resolves under a different parent is still rejected and
reparse-point rejection still runs first. The comparison semantics are recorded
in docs/adr/0038-windows-provider-input-guards.md, and
test/bats/157-provider-input-pathguard-vet.bats covers them offline on any
host. What a real Windows volume reports for a short name is not verified here;
a Windows-host check is requested on this PR.

The branch has since merged main twice (once after
#661, once after
#664). The only conflict was in
docs/KNOWN_FAILURE_MODES.md, where both sides added a section 50: main's
keeps 50 and this PR's two Windows entries are now 51 and 52. No code changed.

#644 overlaps this PR in
acq.backends/common.sh and test/bats/147-provider-input-projection.bats, and
will be rebased onto this one after it merges.

Related Issues

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read CONTRIBUTING.md
  • My commits follow the Conventional Commits format
  • I have tested my changes locally
  • I have updated documentation as needed
  • I have not included any secrets, API keys, or sensitive information

Context: the failure mechanism

acq_backend_terminate runs inside a transaction wrapper: start a guardian,
acquire the provider-input lock, then call the unlocked terminate. Both steps
depend on host facilities Windows does not provide in the POSIX shape:

  • The guardian needs the transaction parent PID, and the lock records the
    guardian's start-time identity. Both came from ps -o, which MSYS lacks, so
    _acq_provider_input_guardian_start returned 1 and acq_backend_terminate
    exited before _acq_backend_terminate_unlocked ran. _acq_msb_ports_teardown
    therefore never ran, and $STUBDIR/state/ports/rmbox.pids survived.
  • The lock path is created through the path guard, which needs
    O_DIRECTORY/O_NOFOLLOW/dir_fd.

Calling _acq_msb_ports_teardown directly worked (rc=0, file removed), which
confirmed the teardown logic itself was sound and the wrapper was the whole
problem.

Fix

Path walk — acq.backends/provider_input_pathguard.py (new). Each component
is opened with CreateFileW using
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS (the O_NOFOLLOW
equivalent, plus the flag a directory handle needs) and vetted with
GetFileInformationByHandleEx(FileAttributeTagInfo): a
FILE_ATTRIBUTE_REPARSE_POINT (symlink, junction, mount point) or a
non-directory is rejected, which is the O_DIRECTORY check. The handle's own
resolved name must equal the parent handle's resolved name plus the component,
which is the descriptor-relative walk's anti-TOCTOU property expressed as an
identity check. Missing components are created with CreateDirectoryW (which
inherits the parent ACL — the Windows analogue of the POSIX 0o755 mkdir
honouring umask). There is no os.makedirs fallback anywhere: rejections
raise, and the caller sees the same SystemExit payload the POSIX path produces.

Why the shell translates the path first. The MSYS runtime rewrites
POSIX-looking argv values for native programs, and that rewrite resolves
junctions and symlinks — a guard fed the rewritten value could not tell a
pre-created link from a real directory and would honour the redirect. So the
shell builds the Windows spelling from /proc/mounts (the MSYS-owned mount
table: longest matching mount point replaced by its Windows form, everything
below appended verbatim, never resolved) and passes that; an already-drive-form
argv value passes through MSYS untouched. cygpath is deliberately not used:
it resolves links inside the path, which is exactly the information the guard
must keep.

Process guards. _acq_provider_input_parent_pid keeps ps -o ppid= first
and falls back to parsing ps -p PID (MSYS prints
PID PPID PGID WINPID TTY UID STIME COMMAND), selecting the row by exact PID
match so it works with or without a header. _acq_provider_input_process_identity
keeps ps -o lstart= first and falls back to procfs /proc/<pid>/stat field 22
(starttime ticks — the value Linux ps derives lstart from). MSYS STIME was
rejected on purpose: one-second resolution cannot distinguish a recycled PID from
the original lock owner. Both paths still fail closed on empty or non-numeric
output.

Test Results

Host: native Windows 11, Git Bash (MINGW64_NT-10.0-26200), native Python
3.12.10 (reports sys.platform == 'win32'), native jq 1.8.2.

Red → green (test/bats/112-msb-ports.bats test 11)

Parent commit ef24965, clean worktree, before any change:

not ok 11 msb ports: rm and stop tear down recorded port state; un-published teardown is a no-op
# (from function `assert' in file test/vendor/bats-assert/src/assert.bash, line 40,
#  in test file test/bats/112-msb-ports.bats, line 209)
#   `assert [ ! -f "$STUBDIR/state/ports/rmbox.pids" ]' failed

This branch (measured before #663 merged, on top of fix/648-msb-ports-crlf):

ok 11 msb ports: rm and stop tear down recorded port state; un-published teardown is a no-op

Full-file summary on the new head: 1..18, 18 ok, no failures, exit 0.
Test 13 also passes here, but only because the base carries
#663's tr -d '\r' fix — on this branch alone
(ef24965 + this branch's guard commits, without #663) test 13 was still red, which is why it was
measured on top of fix/648-msb-ports-crlf; that fix is now in main.

Other suites on this host

These were captured on the pre-merge head (before main was merged in). The rebase changed no code they
exercise: acq.backends/msb.sh is byte-identical to the base (comment wording
only), test/bats/112-msb-ports.bats is byte-identical to the base (this
branch's duplicate CRLF test removed), and acq.backends/common.sh plus the new
module are unchanged.

Suite Result
test/bats/112-msb-ports.bats 18/18 ok (re-run on the new head)
test/bats/146-provider-verification.bats 28/28 ok
test/bats/117-msys-path-forms.bats 11/11 ok (two of these failed before the guard fix)
test/bats/120-workspace-preflight.bats 14/15 ok; identity (b) … symlinked child not followed fails identically on ef24965
test/bats/147-provider-input-projection.bats 20 ok, 15 not ok, then stopped at test 36 (see below)
scripts/run-shellcheck (every touched shell file) clean
markdownlint (.markdownlint.jsonc) on touched docs clean

Every one of the 15 147 failures reproduces on ef24965. I re-ran each on
a clean worktree of that commit: the four setup validator … tests, the two
candidate snapshot … tests, failed validator dependency probe …,
projection prepares immutable generation …, parent exit cannot make a live transaction lock recoverable, unknown backend/msb existence …, rejected replacement …, failed atomic swap …, empty discovery publishes …, and
msb remove TERM …. The candidate snapshot / projection failures are the
deferred _acq_provider_input_snapshot gap (AttributeError: module 'os' has no attribute 'O_DIRECTORY'), recorded in docs/KNOWN_FAILURE_MODES.md rather than
fixed here. The validator tests are POSIX-only by design.

Test 36 (msb create TERM …) changed shape on Windows. On ef24965 it
fails fast with slow-msb-create never started (provisioning aborts at the
guard). With the guard fixed it gets far enough for the test's slow msb create
stub to start, and the run then does not complete (>300 s). The exact blocking
point is unconfirmed and is in the MSYS process/signal model, not in the guard;
it is tracked as failure mode 51 in docs/KNOWN_FAILURE_MODES.md. This is the
one behaviour change in this PR that is not a fix, and it is why the 147 run is
reported as partial rather than green.

What I could not verify locally

  • Linux bats results. Not producible on a Windows host. CI is authoritative
    for the Linux suite and for the bash 3.2 gate
    (.github/workflows/bash32-compat.yml), which runs this same offline suite
    under bash 3.2.57.
  • bash 3.2 locally. No bash 3.2 here. The new code uses only
    case/parameter expansion and ordinary utilities (no ${x,,}, no associative
    arrays, no mapfile); the new tests avoid bash-4-only features.
  • Linux link behaviour. The junction adversary exists only on Windows; the
    tests pick the host's own link mechanism, so Linux exercises the symlink case
    and Windows the junction case for the same invariant.

Rollback

Delete acq.backends/provider_input_pathguard.py, the sys.platform == 'win32'
branches and Windows helpers in acq.backends/common.sh, the added tests, and
the ADR. The POSIX path was never modified, so nothing needs restoring for
Linux/macOS. Nothing in acq.backends/msb.sh is this branch's to roll back.

Security Impact

  • The traversal invariant is preserved, not relaxed: Windows rejects reparse
    points on every component below the mount anchor, and rejects a child that
    resolves outside its vetted parent. No fallback can silently follow a link.
  • Windows is marginally stricter than POSIX: a volume mount point below the
    drive root is rejected, where POSIX would accept a bind mount.
  • Residual difference, documented in ADR-0038: components are opened by name
    (Win32 has no handle-relative open) and the identity check runs on the handle
    just opened. The pre-created-ancestor threat this guard exists for is fully
    covered; a concurrent rename race would need NtCreateFile and is not closed
    here.
  • Deferred and tracked in docs/KNOWN_FAILURE_MODES.md: _acq_provider_input_snapshot
    still uses O_DIRECTORY/O_NOFOLLOW/dir_fd for the fetched kit, so
    provider-input staging remains a Windows gap (fail-closed: it errors out rather
    than copying an unvalidated tree).
  • No new dependencies: standard-library ctypes plus shell builtins/coreutils.

AI-assisted (OpenCode [deepseek-v4.1-flash]); human review required.

A native Windows jq used from Git Bash ends each output line with CRLF. In
_acq_msb_ports_from_inspect the trailing CR made every "H:G" record fail the
digit guard except the last, so `acq ports` under-reported create-time ports.

Strip carriage returns from the jq output before validation and keep the digit
guard. Add a regression test with a stub jq that emits CRLF line endings.

Fixes: #648

Co-authored-by: OpenCode [claude-sonnet-5.5] <bret.mogilefsky@gsa.gov>
@mogul
mogul requested a review from a team as a code owner October 29, 2026 10:25
mogul added 4 commits October 29, 2026 05:41
The provider-input transaction wrapper reads the guardian's parent PID and the
guardian's start-time identity with `ps -o ppid=` and `ps -o lstart=`. MSYS
`ps` (Git Bash) has no `-o` at all - it prints "ps: unknown option -- o" and
nothing else - so the parent PID came back empty and
_acq_provider_input_guardian_start returned 1. Every `acq terminate` then
exited before _acq_backend_terminate_unlocked ran, which is why backend
teardown and the recorded port teardown behind it never happened.

Keep the POSIX commands first so POSIX behaviour is unchanged, and fall back
only when they yield nothing:

- _acq_provider_input_parent_pid parses `ps -p PID`, which MSYS does support,
  selecting the row by exact PID match and taking its PPID column. It is
  correct with or without the header line, and still fails closed on empty or
  non-numeric output.
- _acq_provider_input_process_identity falls back to procfs field 22
  (starttime ticks), the value Linux ps derives lstart from. MSYS STIME was
  rejected deliberately: its one-second resolution cannot distinguish a
  recycled PID from the original lock owner.

Refs: ADR-0038
Windows CPython has no os.O_DIRECTORY, no os.O_NOFOLLOW and no dir_fd support,
so _acq_provider_input_safe_mkdir and _acq_provider_input_safe_dir could not run
there at all. Both now delegate to acq.backends/provider_input_pathguard.py when
sys.platform == 'win32'; the POSIX walk is untouched.

The new module keeps the descriptor-anchored guarantee with the Win32 API: every
component is opened with FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS
(the O_NOFOLLOW equivalent, plus the flag a directory handle needs), rejected
when GetFileInformationByHandleEx reports FILE_ATTRIBUTE_REPARSE_POINT or a
non-directory (the O_DIRECTORY check), and required to resolve to exactly the
vetted parent's name plus the component (the descriptor-relative walk's
anti-TOCTOU property). Missing components are created with CreateDirectoryW,
which inherits the parent ACL. There is no os.makedirs fallback anywhere:
rejections raise the same SystemExit payload the POSIX path produces.

The shell hands the guard a Windows spelling it built from /proc/mounts rather
than letting MSYS rewrite the argument, because that rewrite resolves junctions
and symlinks - a guard fed the resolved path would honour exactly the redirect
it exists to refuse. cygpath is deliberately not used, for the same reason.

Refs: ADR-0038
Add host-neutral regression tests for the two Windows gaps.

The link tests use the host's own mechanism - a POSIX symlink, or a Windows
junction via mklink /J - so one shared traversal invariant is asserted everywhere
instead of a Windows-only truth, and a host that cannot create a link fails the
test rather than skipping it. The rest cover the guardian's transaction parent,
identity stability and fail-closed behaviour on garbage input, and the
POSIX-compatible diagnostic for a relative path.

The msb port-list CRLF defect found during this work is covered by
#663, which this branch is based on.
ADR-0038 explains why the Win32 handle walk is equivalent to the POSIX
O_NOFOLLOW/dir_fd walk, why the path spelling is built from the mount table
instead of cygpath, why procfs starttime replaced STIME for lock identity, and
what residual risk remains (components are opened by name; volume mount points are
rejected).

docs/KNOWN_FAILURE_MODES.md records the two Windows gaps that are deferred and
tracked rather than fixed here: provider-input staging still uses O_DIRECTORY, and
the msb create TERM suite test now reaches (and hangs in) a pre-existing MSYS
process/signal path.
@mogul
mogul force-pushed the fix/windows-provider-input-guard branch from 2309a7a to 9c127c3 Compare October 29, 2026 12:45
@mogul
mogul changed the base branch from main to fix/648-msb-ports-crlf October 29, 2026 12:45
Base automatically changed from fix/648-msb-ports-crlf to main October 29, 2026 14:29
basiliskus
basiliskus previously approved these changes Oct 29, 2026

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: approve — 9c127c3339d1f3287282ae04e8d291260ede699d

No findings. The Windows path guard fails closed, rejects reparse points, preserves the existing POSIX path, and the process-identity fallbacks validate their inputs. The regression tests cover ordinary paths, link ancestors and leaves, parent-PID discovery, and stable process identity.

Native Windows execution was not available in this review environment, and the local Bats suite could not run because its vendored submodules were absent. ShellCheck passed, and the current GitHub checks are green.

Assisted-by: OpenCode [gpt-5.6-sol-latest-guardrails-defaultv2] skill:change-review

…input-guard

Resolve the docs/KNOWN_FAILURE_MODES.md conflict introduced when
#661 merged. Both sides appended a section 50:
main's "SBX: Readonly Kit Files Are Guest-Resident Copies" keeps number 50, and
this branch's two Windows entries are renumbered to 51 (provider-input staging)
and 52 (msb create TERM suite hang). Nothing in the tree referenced the old
numbers. No code changed; common.sh and the 147 bats file merged cleanly.

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: request changes — 6ea1758ebd86f9fef9bdb48a3ad853e1f8ec8c1f

  • Major — acq.backends/provider_input_pathguard.py:207-212: Literal short-name components can compare unequal to normalized handle names, aborting teardown for legitimate Windows paths. The comparison failure reproduces with production _vet and ntpath; native Windows confirmation remains needed. Preserve reparse rejection, compare the normalized child’s parent against the vetted parent, and add short-name regression coverage.

Assisted-by: OpenCode [gpt-6.1-sol-latest-guardrails-defaultv2] skill:change-review

The component check compared the handle's resolved name with the literal
component text, so a legitimate path containing an 8.3 alias (PROGRA~1,
RUNNER~1) was rejected as redirected and aborted teardown. The kernel reports
the long name for such a handle.

Compare the resolved child's parent directory with the vetted parent's resolved
path instead, and require the leaf to be a real name (no dot components, no
stream). Reparse-point and directory attribute rejection still run first. Use
ntpath explicitly for Windows path logic so it behaves identically off Windows,
strip the \\?\UNC\ prefix case-insensitively, and reject a colon in any input
component.

Add an offline harness that drives the real walk over a fake volume, and record
the comparison semantics in ADR-0038. Short-name behaviour on a real volume is
not verified here.

Refs: #667

Co-authored-by: OpenCode [claude-sonnet-5.5] <bret.mogilefsky@gsa.gov>
@mogul
mogul requested a review from basiliskus October 31, 2026 18:52
@mogul

mogul commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

🤖: AI agent (OpenCode), posting on behalf of @mogul. Request: Windows host validation of the new head of this PR. Head 262a53a4 (fix(acq): accept 8.3 short names in the windows provider-input guard) answers the review finding that a legitimate path containing an 8.3 short-name alias could be rejected as "redirected". The comparison logic is covered offline by test/bats/157-provider-input-pathguard-vet.bats (32 checks over a fake volume), but what a real Windows volume reports for a short-name handle has never been observed; only a Windows host can confirm that. If a step fails for an environment reason, report it as BLOCKED with the exact error and do not work around it silently. Do not paste any token or API key values into the reply.

Tested revision: 262a53a4 (give the full SHA). If fix/windows-provider-input-guard has moved, test the new head and say which SHA.

Host requirements: real Windows (not WSL), native Python 3.12 (not MSYS Python), an elevated cmd.exe for steps 1, 2 and 5 (mklink /J itself does not need elevation, fsutil does). Use a scratch directory C:\acq83; delete it afterwards. Run the Python commands from the repo root of a clean checkout of the tested SHA.

:: 0. Environment (paste output)
python -c "import os,sys; print('os.name=',os.name,'platform=',sys.platform,sys.version.split()[0])"
git rev-parse HEAD
ver

:: 1. Is 8.3 name creation enabled on C:? (paste output)
fsutil 8dot3name query C:
::   If creation is disabled, REPORT that, then enable it for this test only and restore the original value afterwards:
::   fsutil 8dot3name set C: 0
::   Existing directories do not get short names retroactively, so create the test tree AFTER enabling it.

:: 2. Create a long-named tree and find its alias (paste output of dir /x)
mkdir "C:\acq83\Long Directory Name\tree"
dir /x C:\acq83
::   Expect a row like:  LONGDI~1    Long Directory Name   (note the exact alias; use it below in place of LONGDI~1)

:: 3. Does the kernel report the LONG name for the alias handle? (the premise of the fix)
python -c "import sys; sys.path.insert(0,'acq.backends'); import provider_input_pathguard as g; h=g._open_directory(r'C:\acq83\LONGDI~1'); print(g._final_path(h)); g._close(h)"
::   Expect: C:\acq83\Long Directory Name

:: 4. The guard accepts a legitimate path that goes through the alias
python -c "import sys; sys.path.insert(0,'acq.backends'); import provider_input_pathguard as g; g.safe_dir(r'C:\acq83\LONGDI~1\tree'); print('accepted')"
echo exit=%ERRORLEVEL%
::   Expect: accepted, exit=0.
::   Baseline: repeat step 4 at 6ea1758e (git checkout --detach 6ea1758e, same command). Expect exit=1 (rejected as redirected). Then return to the tested SHA.

:: 5. Redirects are still refused
mklink /J C:\acq83\evil C:\Windows
python -c "import sys; sys.path.insert(0,'acq.backends'); import provider_input_pathguard as g; g.safe_dir(r'C:\acq83\evil\System32')"
echo exit=%ERRORLEVEL%
::   Expect: exit=1, no traceback. Also try an alias that points through the junction:  dir /x C:\acq83  (is there a short name for "evil"?) and repeat with that alias; expect exit=1.

:: 6. safe_mkdir with an alias path creates the child under the long-named directory
python -c "import sys; sys.path.insert(0,'acq.backends'); import provider_input_pathguard as g; g.safe_mkdir(r'C:\acq83\LONGDI~1\made'); print('made')"
dir "C:\acq83\Long Directory Name"
::   Expect: made, and a directory named "made" listed under "Long Directory Name" (not a second tree).

Then, from Git Bash on the same host and checkout:

# 7. Existing suites that cover the guard (paste the TAP summary and every 'not ok' line)
git submodule update --init test/vendor/bats-core test/vendor/bats-support test/vendor/bats-assert
./scripts/test-acq-bats test/bats/157-provider-input-pathguard-vet.bats test/bats/112-msb-ports.bats test/bats/117-msys-path-forms.bats
# 147 can hang on this host (see the KNOWN_FAILURE_MODES entry for the msb create TERM test); run it with a per-test timeout or skip that test, and say which:
#   ./scripts/test-acq-bats -f 'windows:' test/bats/147-provider-input-projection.bats

# 8. Real teardown through a short-name workspace path (only if a sandbox backend is usable; otherwise report BLOCKED for this step)
#    Place a workspace under a path containing an 8.3 alias, e.g. a directory under C:\acq83\LONGDI~1, run acq create/terminate or the 112 stub flow against it, and confirm teardown runs (no 'unsafe provider-input path').

Expected (judge against these): step 3 prints the long name; step 4 accepted/exit=0 here and exit=1 at 6ea1758e; step 5 exit=1 with no traceback; step 6 creates made under the long-named directory; step 7 157 is ok, 112 is 18/18, 117 is 11/11, and the windows: tests in 147 pass or skip with a stated reason. Earlier runs on this host had 147 blocked on the MSYS signal model and a few unrelated pre-existing failures; list any failure and say whether it also fails at 6ea1758e.

Please report: step 0 and 1 output, the tested SHA, the output of steps 2 to 6 verbatim, the step 7 TAP lines, whether step 8 ran, and an explicit PASS, FAIL or BLOCKED verdict per step plus an overall verdict. Clean up: rmdir /s /q C:\acq83 (remove the junction first with rmdir C:\acq83\evil) and restore the original fsutil 8dot3name setting if you changed it.

@mogul

mogul commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

🤖: AI agent (OpenCode [deepseek-v4.1-flash]) on the Windows host rocinante-w11, posting on behalf of @mogul. Windows validation of 262a53a4: BLOCKED (partially verified)

Tested revision

  • SHA tested: 262a53a4f3618004f9d16acfd9fec431513a6acf (fix(acq): accept 8.3 short names in the windows provider-input guard), preceded by merge 6ea1758ebd86f9fef9bdb48a3ad853e1f8ec8c1f. The branch had not moved when fetched. Worktree detached at the tested SHA; nothing committed, pushed, rebased or edited.
  • Host: native Windows, Microsoft Windows [Version 10.0.26200.9457]; native CPython (os.name= nt platform= win32 3.12.10); Git Bash under C:\Program Files\Git\bin\bash.exe for step 7 only (bash on PATH is WSL).
  • Token context: this is a filtered Medium-integrity admin token, so fsutil cannot open the volume at all (step 1), which is what blocks steps 1 and 2.

Verdict per step

Step Verdict Evidence (one line)
0 Environment PASS os.name= nt platform= win32 3.12.10; HEAD 262a53a4…
1 fsutil 8dot3name query C: BLOCKED (elevation) Failed to open the volume. / Error 5: Access is denied. (exit 1); no set attempted
1b Empirical 8.3 creation PASS (finding: disabled) dir /x C:\acq83 → Long Directory Name with a blank alias column
2 Create a fresh long-named alias BLOCKED (no 8.3 creation) Cannot create an alias; substituted pre-existing aliases (step 3)
3 Kernel reports long name for alias PASS D3DSCA~1 → C:\Users\bmogi\AppData\Local\D3DSCache; PROGRA~1 → C:\Program Files
4 Guard accepts alias path PASS (red/green) 262a53a4: accepted, exit 0 · 6ea1758e: exit 1, silent, redirected … (expected '…D3DSCA~1')
5 Redirects still refused PASS — alias-through-junction sub-case BLOCKED junction → C:\Windows: exit 1, silent, reparse point at 'C:\acq83\evil'; no alias for evil in dir /x
6 safe_mkdir through alias PASS made, exit 0; child under long name D3DSCache, same object via alias
7 Suites PASS for 157/112/117 (30/30 ok); windows: filter matches nothing; 147 partial (below) TAP below
8 Teardown through alias path PASS (stub flow; no VM) acq_backend_terminate rc=0, port state removed, no unsafe provider-input path, tree under D3DSCache

Overall: BLOCKED (partially verified). The accept/reject behaviour and the kernel-long-name premise are verified on a real volume using pre-existing aliases. Creating a fresh 8.3 alias is impossible on this host, so that path — and the alias-through-junction sub-case of step 5 — remain unverified.

Step 0 — environment

os.name= nt platform= win32 3.12.10
262a53a4f3618004f9d16acfd9fec431513a6acf
Microsoft Windows [Version 10.0.26200.9457]

Step 1 — fsutil 8dot3name query C: → BLOCKED (elevation)

Failed to open the volume.
Error 5: Access is denied.

Exit code 1. fsutil 8dot3name set C: 0 was not attempted: it cannot succeed with this token and it is a volume policy change.

Substituted empirical check — mkdir "C:\acq83\Long Directory Name\tree" then dir /x C:\acq83:

 Volume in drive C is Windows
 Volume Serial Number is 8E00-D7F4

 Directory of C:\acq83

10/31/2026  12:49 PM    <DIR>                       .
10/31/2026  12:49 PM    <DIR>                       Long Directory Name
               0 File(s)              0 bytes
               2 Dir(s)  170,773,327,872 bytes free

The short-name column is blank: no alias is generated, so 8.3 creation is effectively disabled on C:. Pre-existing directories keep theirs — dir /x C:\Users\bmogi\AppData\Local shows D3DSCA~1 D3DSCache, CRASHD~1 CrashDumps, CHOCOL~1 Chocolatey GUI.

Step 2 — create a long-named tree and read its alias → BLOCKED

No alias can be created here (8.3 creation disabled and enabling it is elevation-blocked), so .\\LONGDI~1 does not exist and step 3/4/6 could not use a freshly created alias. Substituted pre-existing aliases:

  • C:\Users\bmogi\AppData\Local\D3DSCA~1 → D3DSCache (writable, so it also serves step 6)
  • C:\PROGRA~1 → C:\Program Files (second data point)

Step 3 — does the kernel report the long name for an alias handle? → PASS

g._final_path(g._open_directory(r'C:\Users\bmogi\AppData\Local\D3DSCA~1')):

C:\Users\bmogi\AppData\Local\D3DSCache

g._final_path(g._open_directory(r'C:\PROGRA~1')):

C:\Program Files

The premise of the fix holds on a real Windows volume.

Step 4 — the guard accepts a legitimate path through the alias → PASS (red/green)

At the tested SHA 262a53a4:

HEAD=262a53a4f3618004f9d16acfd9fec431513a6acf
accepted
exit=0

At baseline 6ea1758e (git checkout --detach 6ea1758e, same command):

HEAD=6ea1758ebd86f9fef9bdb48a3ad853e1f8ec8c1f

exit=1

It fails silently — safe_dir raises SystemExit(1) with a numeric code, so nothing is printed. To show why, I also called _walk(...) directly at both heads:

accepted (no exception)
UnsafePath: redirected 'C:\\Users\\bmogi\\AppData\\Local\\D3DSCA~1' -> 'C:\\Users\\bmogi\\AppData\\Local\\D3DSCache' (expected 'C:\\Users\\bmogi\\AppData\\Local\\D3DSCA~1')

That is exactly the review finding. HEAD was restored to 262a53a4 afterwards (verified).

Step 5 — redirects are still refused → PASS

Junction created for C:\acq83\evil <<===>> C:\Windows

safe_dir(r'C:\acq83\evil\System32'), output empty, exit 1, no traceback. Internal reason:

UnsafePath: reparse point at 'C:\\acq83\\evil'

dir /x C:\acq83:

 Volume in drive C is Windows
 Volume Serial Number is 8E00-D7F4

 Directory of C:\acq83

10/31/2026  12:50 PM    <DIR>                       .
10/31/2026  12:50 PM    <JUNCTION>                  evil [C:\Windows]
10/31/2026  12:49 PM    <DIR>                       Long Directory Name
               0 File(s)              0 bytes
               3 Dir(s)  170,757,832,704 bytes free

No alias exists for evil, so "an alias that points through the junction" could not be exercised — BLOCKED on the same 8.3-creation limitation as step 2.

Step 6 — safe_mkdir through the alias → PASS

safe_mkdir(r'C:\Users\bmogi\AppData\Local\D3DSCA~1\acq83made'):

made
exit=0
 Directory of C:\Users\bmogi\AppData\Local\D3DSCache
10/19/2026  11:02 PM    <DIR>                       ..
10/31/2026  12:50 PM    <DIR>                       acq83made

Same object through the alias (no second tree):

 Directory of C:\Users\bmogi\AppData\Local\D3DSCA~1
10/31/2026  12:50 PM    <DIR>                       acq83made

Directory removed afterwards (exists-after-cleanup=False).

Step 7 — suites

The -f 'windows:' filter selects nothing in 147, so that part could not be followed as written:

ERROR: Found no tests. (Try `--allow-empty-suite`?)1..0
rc=1

grep -c '@test "windows:' test/bats/147-provider-input-projection.bats → 0; the file has 71 tests, and no test in the repository is named windows: ….

./scripts/test-acq-bats test/bats/157-provider-input-pathguard-vet.bats test/bats/112-msb-ports.bats test/bats/117-msys-path-forms.bats:

1..30

30/30 ok, exit 0, zero not ok lines — 157 1/1 (path guard: component vetting accepts 8.3 aliases and rejects redirects), 112 18/18 (including ok 12 msb ports: rm and stop tear down recorded port state; un-published teardown is a no-op and ok 15 msb ports: LIST mode lists every port when jq emits CRLF line endings (Windows)), 117 11/11. Numbering is from this combined run.

147, time-boxed with the documented hang excluded (--negative-filter 'msb create TERM', timeout 4200): did not complete. 19 ok / 15 not ok through test 34, then it stalled in msb remove TERM: external executable survives initial signal, second lock blocked until exit (test 35) and was stopped by the time box; no 1.. summary line was produced. msb remove TERM is the same MSYS TERM/signal family as the documented msb create TERM hang (KNOWN_FAILURE_MODES 51); it also did not complete at 6ea1758e. Note for future runs: BATS_TEST_TIMEOUT is unusable on this host — bats' own timeout cleanup calls ps -Ao pid,ppid,args, which MSYS ps does not support, giving bats-exec-test: line 249: ppid_col: unbound variable.

not ok lines observed (15):

not ok 4 setup validator honors explicit bootstrap Python and online hash install
not ok 5 setup validator prefers a supported versioned Python before python3
not ok 6 setup validator skips unsupported newer Python on Linux
not ok 8 setup validator atomically replaces only a validated temporary runtime
not ok 13 failed validator dependency probe publishes nothing
not ok 14 projection prepares immutable generation without changing current
not ok 16 candidate snapshot preserves spec.yaml for host allowlist validation
not ok 17 candidate snapshot rejects malformed provider input manifest with actionable error
not ok 21 parent exit cannot make a live transaction lock recoverable
not ok 22 guardian retains lock while registered backend child survives transaction
not ok 23 unknown backend existence preserves prior projection and skips replacement
not ok 24 unknown msb existence preserves prior projection and skips replacement
not ok 28 rejected replacement preserves prior current generation
not ok 29 failed atomic swap preserves prior current generation
not ok 30 empty discovery publishes authoritative empty generation

All 15 also fail at 6ea1758e — I re-ran exactly this set there (git checkout --detach 6ea1758e), and every one came back not ok:

1..15
not ok 1 setup validator honors explicit bootstrap Python and online hash install
not ok 2 setup validator prefers a supported versioned Python before python3
not ok 3 setup validator skips unsupported newer Python on Linux
not ok 4 setup validator atomically replaces only a validated temporary runtime
not ok 5 failed validator dependency probe publishes nothing
not ok 6 projection prepares immutable generation without changing current
not ok 7 candidate snapshot preserves spec.yaml for host allowlist validation
not ok 8 candidate snapshot rejects malformed provider input manifest with actionable error
not ok 9 parent exit cannot make a live transaction lock recoverable
not ok 10 guardian retains lock while registered backend child survives transaction
not ok 11 unknown backend existence preserves prior projection and skips replacement
not ok 12 unknown msb existence preserves prior projection and skips replacement
not ok 13 rejected replacement preserves prior current generation
not ok 14 failed atomic swap preserves prior current generation
not ok 15 empty discovery publishes authoritative empty generation

None is a regression from 262a53a4. The candidate snapshot / projection ones are the deferred _acq_provider_input_snapshot Windows gap; the setup validator ones are POSIX-only; the two lock/guardian ones are the MSYS signal model.

Because the full-file run stalled before the path guard: tests (they sit near the end of the file), I ran that subset explicitly on the tested SHA:

1..8
ok 1 path guard: a pre-created link ancestor receives no created components
ok 2 path guard: safe_dir rejects a pre-created link ancestor
ok 3 path guard: a link as the final component is rejected
ok 4 path guard: a relative path is rejected with the POSIX diagnostic
ok 5 path guard: an ordinary nested path is created and then accepted
ok 6 guardian start discovers a numeric transaction parent on this host
ok 7 process identity is stable for a live pid and fails closed otherwise
ok 8 transaction parent pid matches the host's own answer and fails closed

Step 8 — real teardown through an alias path → PASS (stub flow, no VM)

With ACQ_PROVENANCE_DIR='/c/Users/bmogi/AppData/Local/D3DSCA~1/acq83prov' (MSYS spelling keeps the alias; the guard maps it via /proc/mounts without resolving), driving the real acq_backends/msb.sh terminate path with the repo's own stub library:

pids file after publish: present
terminate rc=0
terminate output: <none>
unsafe-path error present: 0
pids file after terminate: removed
landed under long name D3DSCache
provider-input dir:
/c/Users/bmogi/AppData/Local/D3DSCache/acq83prov
/c/Users/bmogi/AppData/Local/D3DSCache/acq83prov/provider-input
/c/Users/bmogi/AppData/Local/D3DSCache/acq83prov/provider-input/msb

The guarded teardown ran to completion with a provenance tree addressed through the alias, the port teardown actually happened (recorded state removed), and the tree was created under the long name.

Caveat: this is the stub flow the request allows as an alternative to a real sandbox; no VM was created. msb 0.7.11 is installed (sbx is not), but a real msb create against an alias-containing workspace was not attempted within the time box — it is a multi-minute VM boot plus kit fetch, not a quick check.

What this does not verify

  • New-alias creation (steps 1–2, and the alias-through-junction sub-case of step 5). fsutil 8dot3name query C: fails with Error 5: Access is denied under this filtered Medium-integrity token, so fsutil … set is impossible, and 8.3 creation is disabled, so a freshly created long-named directory gets no alias. Every short-name result above comes from pre-existing aliases (D3DSCA~1, PROGRA~1): the same kernel path, but not a freshly created alias.
  • A VM-backed sandbox teardown addressed through an alias; step 8 ran the stub flow.
  • bash 3.2 / Linux results — not producible on this host; CI remains authoritative for those.

Cleanup

  • rmdir C:\acq83\evil (junction, first) then rmdir /s /q C:\acq83 — both rc 0.
  • D3DSCache\acq83made and D3DSCache\acq83prov removed.
  • Verified gone: C:/acq83 gone, acq83prov gone, acq83made gone.
  • No fsutil setting was changed: only query was ever run, and it fails with Error 5: Access is denied both before and after.

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