Skip to content

Windows: narrowed but non-zero check-to-write race in the hook relay's event write #494

Description

@pbean

What

On Windows the hook relay's event write still has an open (but narrowed)
check-to-write window. src/bmad_loop/data/bmad_loop_hook.py::_write_event
refuses a redirected events/ dir (symlink or junction), then — on the fallback
path only — re-resolves the directory by path for the os.open and the
os.replace.

PR #493 narrowed this: the redirect check runs again after the payload is
written and before it is published, so a junction that is swapped in and left
in place
is refused and the temp file removed. What remains is a
swap-and-restore inside the window: replace events/ with a junction, let the
relay create its temp file inside the attacker's directory, then restore the
real directory before the post-write check runs.

Why it is not already fixed

POSIX closes this properly — the create+rename are anchored to a dir_fd opened
O_RDONLY|O_DIRECTORY|O_NOFOLLOW, so no later path resolution happens.

Windows has no equivalent reachable from the standard library:
os.supports_dir_fd is empty there (dir_fd is implemented with the POSIX *at
calls), and Python exposes no handle-relative open. Closing it properly needs a
directory handle from CreateFileW with
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, and then
NtCreateFile with OBJECT_ATTRIBUTES.RootDirectory set to it — i.e. ctypes
against ntdll, inside a relay that is stdlib-only by contract and runs
under whatever interpreter the host happens to have. That is disproportionate to
the residual risk and carries its own hazard: a bug in it stops hook delivery on
Windows, which stalls every run — the exact harm being defended against.

Residual risk

Low. The attacker must win a precisely-timed race against a hook that fires at
moments they do not control, and must restore the real directory immediately
after. Failing the race in either direction is safe: a persistent junction is
refused (before or after the write), and a refusal degrades to the same
session_timeout_min path the run would have hit anyway. No silent redirect
survives unless the restore is perfectly timed.

Options if this is ever picked up

  1. Accept and document (status quo). The narrowed window is recorded in the
    _write_event docstring.
  2. ctypes + NtCreateFile handle-relative open on Windows. Correct, but a
    large Windows-CI-only-testable surface in a stdlib-only relay.
  3. Move the relay's event writes out of the agent-writable tree entirely, which
    removes the attacker's ability to touch events/ at all. This overlaps with
    the direction of init vendors the hook relay into the agent-writable workspace, turning a file-write primitive into unattended persistent code execution #461 Phase 2 and is probably the cheapest real close.

Option 3 is the one worth evaluating first.

Follow-up from #461 Phase 1 (PR #493), raised by an automated reviewer on that PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions