You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Produce the bounded cross-platform design and prototype evidence required to eliminate selected-file replacement races in ordinary analysis. The target design is one handle-validated acquisition per applicable selected file and one immutable, invocation-owned byte snapshot consumed by every guard.
This issue is the design/prototype gate, not the production implementation. A dependent implementation issue may be authorized only after this issue chooses feasible Windows, Linux, and macOS contracts, resource limits, and public unavailable behavior.
Current behavior and reachable race
Scope resolution loads configuration, resolves explicit or Git-derived scope, canonicalizes candidates with Path.resolve(), and freezes only SelectedFile.reporting_path plus SelectedFile.physical_path in AnalysisContext. Explicit files outside the repository root are supported and use an absolute reporting identity. Recursive and Git selection omit selection-time symlink entries; all explicit, recursive, --changed-only, --staged, and --base-ref modes then converge on the same pathname-based guard API.
Ordinary analysis later reopens selected pathnames independently:
LOC opens UTF-8 text with errors="ignore" in guards/loc.py.
Syntax and Vue read bytes in analysis/regions.py through the syntax pipeline.
Markdown reads strict UTF-8 text in markdown/scanner.py.
Full ordinary analysis can therefore analyze two or three different objects or byte versions. LOC-only, syntax-only, and Markdown-only runs each retain the selection-to-open replacement race. The race applies equally to explicit, recursive, and Git selector modes and to selected in-root files; explicit outside-root files have the same replacement problem even though root-containment is not their authority.
When a LOC baseline is loaded, run_analysis freshly resolves and validates selected paths before guards run. That pathname check narrows stale selection but remains separated from every later open, so it does not close final-component or ancestor replacement. Syntax/provider unavailability is currently retained per file; LOC, Markdown, and ordinary filesystem read failures can abort the invocation. Baseline create/update are separate write modes that directly reread pathnames and do not run ordinary analysis.
Threat model and severity
The race is technically reachable when a concurrent actor can rename, replace, or mutate a selected pathname or a relevant ancestor while Code Guard runs. Code Guard counts and parses repository content; it does not execute selected source.
Potential impact is outside-root derived-data disclosure through counts, callable identities, headings, locations, errors, or output; inconsistent findings; nondeterminism; denial of analysis; and changed aggregate or process exit state. Practical risk is low for a developer-controlled checkout, isolated CI, or a same-user actor with equivalent access. Risk is greater when a privileged scanner analyzes hostile or shared writable worktrees and publishes results across a privilege boundary.
This is defense-in-depth. It is not presently a release blocker or security advisory because no supported deployment establishes that concrete cross-privilege boundary. Reclassification requires such a deployment or a stronger demonstrated exploit; severity must not be raised merely because the race is technically reachable.
Exact security properties
The prototype must define and prove these terms before implementation:
The trusted in-root authority is an opened canonical analysis-root directory object, established before descendant traversal. A mutable root pathname or lexical string prefix is not the authority.
In-root containment means the final regular-file object was reached from that root object through the accepted component policy without following a symlink or Windows name-surrogate reparse point. The root object remains authoritative if its pathname is renamed or replaced.
An explicitly selected outside-root file remains supported unless the prototype documents and justifies an intentional public change. It needs a separate opened anchor and/or selection identity. The prototype must state whether authorization is the canonical target path or the selection-time file identity and how an opened object is proven to satisfy it without relying on a check-then-open pathname sequence.
Final-component symlinks and ancestor symlinks are rejected during acquisition. Windows junctions and other name-surrogate reparse points receive an explicit reject/follow table; no unlisted reparse tag may silently receive a weaker check.
Mount points and hard links are not automatically "outside" objects. Component-relative traversal can cross a mounted filesystem, and a hard-linked inode has no unique parent provenance. The prototype must choose and document whether existing mount points are accepted. It must not claim hard-link provenance protection. A stricter no-mount/volume policy is allowed only with portable evidence and an explicit compatibility decision.
Rename, unlink, or pathname replacement after a validated handle is opened cannot redirect that handle. After capture completes, all guards use the immutable captured bytes, so later pathname or file mutation cannot split invocation evidence.
The promised snapshot is containment plus invocation-internal consistency after capture, not an atomic point-in-time filesystem snapshot. Active in-place writes during capture are rejected when observable by the chosen pre/post identity and size checks, bounded expected-length read, and EOF check. Same-size writes or metadata restoration may be undetectable; no stronger claim is allowed without a proven platform primitive.
"Secure open", "equivalent mechanism", and "fail closed" are not acceptance language. Every unsupported path/capability must map to a named observable result or an invocation error specified below; no pathname-only fallback is permitted.
Platform contract
The prototype must publish a Windows/Linux/macOS capability table covering supported Python 3.10-3.14 installations and the documented x86-64/ARM64 binary envelope. For each platform, record the exact APIs, flags, returned identity, path forms, error mapping, and deterministic tests.
POSIX investigation must cover:
an opened root directory descriptor;
component-by-component os.open(..., dir_fd=...) traversal with O_DIRECTORY and O_NOFOLLOW, followed by a final O_NOFOLLOW open and fstat regular-file/identity checks;
ancestor replacement while traversal is in progress and root-path rename/replacement;
runtime capability checks for optional constants and dir_fd support;
Linux and macOS behavior separately.
Final-component-only O_NOFOLLOW is insufficient. /proc/self/fd is Linux-specific and must not be required on macOS. Linux openat2 may be an optional implementation only; it is not a portable requirement and must not create a silent weaker fallback.
Windows investigation must cover ordinary open/os.open limitations and a bounded handle-based prototype using appropriate CreateFileW reparse/share flags or another demonstrated API. Validation must operate on the opened handle and account for file/volume identity, junctions and other name-surrogate reparse points, GetFinalPathNameByHandleW normalization, long paths, UNC paths, drive-letter casing, volume GUID/device aliases, and root/ancestor replacement. A string-prefix comparison is insufficient. The prototype must decide whether a small audited ctypes boundary, native helper, or dependency is necessary and prove packaging/architecture implications. A broad custom filesystem subsystem is a no-go without a separate product decision.
A supported CI platform may not skip its promised containment test. If a primitive is unavailable on a platform inside the supported envelope, the prototype must choose an explicit analysis-unavailable or invocation-error contract, or recommend narrowing platform support; it must never silently downgrade validation.
Architecture and ownership boundary
Keep SelectedFile responsible for stable selection/reporting identity. The preferred model is a distinct frozen source object that references that identity and owns immutable bytes plus only the acquisition metadata needed for diagnostics. Acquisition belongs in a runner-owned source-acquisition phase after enabled-guard applicability is known, not in individual guards and not in scope discovery unless the prototype proves handles must be retained from that point.
There must be one production source interface: LOC, syntax/Vue, Markdown, unavailable reporting, and future guards consume the captured source and never reopen physical_path. Syntax regions receive the original bytes without a whole-file copy. LOC must preserve the current UTF-8 errors="ignore", universal-newline, comment, and blank-line behavior. Markdown must preserve strict UTF-8 decoding and line semantics. Multiple guards for one file must be able to prove they consumed the identical byte object/value.
Prefer lazy, memoized acquisition once per file in the union of enabled applicable guards, then release the snapshot after every applicable guard finishes. Do not eagerly capture excluded or inapplicable files. LOC-only execution must neither import nor instantiate Tree-sitter. If acquisition for one file fails, no guard may emit partial facts for that file, while completed independent files and guards remain available under the chosen public contract.
Reject dual production APIs in which any guard still accepts a selected pathname while another accepts a snapshot.
Mutation semantics
The prototype and dependent contract must specify these deterministic outcomes:
Replacement before acquisition: accept only the object authorized by the declared root/outside-root policy; otherwise report source acquisition unavailable or the documented invocation error.
Replacement during traversal/open/validation: never follow an unaccepted symlink/reparse component and never emit facts from an unvalidated object.
Rename/unlink/replacement after successful handle validation: finish capture from the already-open object, subject to mutation checks.
Replacement after capture: all guards analyze captured bytes; the new pathname object is irrelevant.
Deletion between selection and acquisition or unreadable/non-regular input: one deterministic acquisition failure, with no partial findings for that file.
In-place truncation/growth during capture or observable pre/post identity/size change: reject the capture. Read the declared size with bounded chunks and verify expected length and EOF.
Undetectable same-size in-place overwrite during capture: outside the atomic-snapshot guarantee and documented as such. The captured byte sequence is nevertheless immutable and authoritative for all guards in that invocation.
Do not rely on advisory locks. Windows sharing modes may reduce concurrent mutation but cannot revoke an already-open writer and must not be described as a complete guarantee.
Resource limits
The prototype must measure representative repository/file sizes and select a documented numeric default per-file byte limit and a bounded live aggregate-memory strategy before production implementation is authorized. It must show that lazy acquisition and prompt release do not undo #122's ordinary-analysis performance gains for repositories with thousands of selected files.
Capture must stream in bounded chunks and avoid duplicate whole-file decoded-text caches. Bytes may be shared directly; guard-specific decoding may be transient. An oversized file is a deterministic source-acquisition failure, never a partial finding or silent skip.
Temporary spooling is disallowed for the first implementation unless prototype evidence shows that a reasonable byte cap cannot preserve supported use. If later authorized, the design must require an OS temp location outside the repository, exclusive owner-only creation where supported, no pathname reopen by guards, read-only/immutable consumption, deterministic cleanup, and cleanup diagnostics that never replace the primary failure. No persistent cache metadata is allowed.
Compatibility requirements
Except for the explicitly approved acquisition-unavailable addition, preserve human output; full/debug/compact JSON; finding values and source locations; deterministic ordering; selected/analyzed/skipped/excluded counts; required policies; existing unavailable entries; aggregate states; exit codes; INCOMPLETE behavior under --ci; exclusions; loaded-baseline semantics; explicit and Git selectors; selection-time symlink behavior; thresholds and default guards; ordinary-analysis read-only behavior; and LOC-only parser dormancy.
The prototype must specify the public shape and ordering of source-acquisition unavailability: its stable kind/message/path, which applicable guard IDs become incomplete, how scope counts change, and whether non---ci and --ci exits change. Completed evidence for other files/independent guards must be retained. This is an intentional public contract decision, not an implementation detail.
Baseline create/update write modes are outside the ordinary-analysis implementation slice. The prototype must document their analogous pathname race and either place them explicitly in the dependent implementation issue or justify a later dependent issue; preserving baseline semantics does not imply that these write modes are already protected.
Any proposed change to outside-root support, mounts, reparse handling, scope counts, output, or exits must be called out for approval rather than hidden as hardening.
Deterministic validation
Use deterministic gates at the real production acquisition seam; no sleeps, retry races, wall-clock assertions, or exhaustive guard x selector x platform matrix.
The prototype must demonstrate:
final in-root pathname replacement with an outside-root symlink before acquisition is rejected;
ancestor replacement with a POSIX symlink and a Windows junction/name-surrogate reparse point is rejected;
root pathname replacement does not change the opened root authority;
post-capture pathname/file mutation cannot change any enabled guard's evidence;
one mixed LOC/syntax/Markdown fixture yields facts from identical captured bytes and no guard reopens the selected pathname;
unchanged normal files and the chosen explicit outside-root policy work;
declared mount, hard-link, long-path, UNC, drive/volume-alias, and reparse policies have focused evidence where applicable;
oversized, deleted, unreadable, and observably mutated sources produce the selected deterministic failure;
one failed acquisition retains completed independent evidence and produces the specified incomplete state;
LOC-only acquisition keeps Tree-sitter dormant and normal behavior retains CRLF/UTF-8 byte-column/Vue/Markdown/LOC compatibility through focused coverage plus the existing suite.
Capability-local skips are allowed only outside a promised supported primitive. A skipped security guarantee on Windows, Ubuntu, or macOS CI is not a pass. Tests must assert shipped behavior, not mocks, wrappers, or helpers for their own sake.
Non-goals
Preventing mutation before selection begins or an attacker modifying the Code Guard process, installation, or runner.
Executing, sandboxing, or judging the trustworthiness of repository content.
An atomic filesystem snapshot against undetectable in-place writers unless the prototype proves one.
Hard-link provenance, and mount exclusion unless explicitly selected after compatibility evidence.
Persistent caches, repository metadata, broad telemetry, parallelism, new guard behavior, threshold changes, guard disabling, output suppression, or a generalized filesystem framework.
Production implementation, PR creation, release work, or security-advisory publication in this issue.
Acceptance criteria
A reviewed Windows/Linux/macOS capability table records exact APIs/flags, root and outside-root authority, supported path forms, reparse/symlink/mount behavior, file/volume identity, failures, and capability detection.
Deterministic prototypes prove final-component and ancestor replacement rejection on each supported CI platform; none of those guarantee tests is skipped.
The Windows result selects and justifies ctypes, a bounded helper, or a dependency and documents Python 3.10-3.14 plus x86-64/ARM64 packaging consequences.
The POSIX result proves component-relative traversal on Linux and macOS and treats openat2 and /proc only as optional/Linux-specific mechanisms.
Exact containment, explicit outside-root, symlink/reparse, mount, hard-link, rename/unlink, and in-place-mutation promises are documented without overstating atomicity.
A distinct immutable source ownership model and one no-path-reopen guard interface are selected; LOC decoding, Markdown decoding, byte/source locations, and LOC-only parser dormancy are mapped.
Source-acquisition unavailable output, ordering, scope counts, incomplete guard IDs, aggregate state, and --ci/non-CI exits are specified with examples, including retained independent evidence.
Baseline create/update sequencing is explicitly assigned; no ordinary-analysis claim silently covers those separate write modes.
The prototype ends with a documented go/no-go. A no-go identifies the smallest unsupported guarantee/product decision; it does not silently ship pathname validation.
No shipped production behavior, repository metadata, installation, benchmark corpus, threshold, guard default, or release state is changed by this design/prototype issue.
Release classification
Security hardening, currently non-blocking for the developer-controlled and isolated-CI threat model. #122 and the current release line remain unaffected unless a concrete supported cross-privilege deployment is established. This issue produces design evidence only and is not itself release work.
Implementation sequencing
Split delivery:
Complete Harden analysis with immutable, handle-validated source snapshots #125 as the bounded design/prototype contract above. Prototype code may be throwaway evidence, but it must not alter shipped behavior. Review the capability matrix, deterministic platform evidence, resource measurements, public unavailable contract, dependency/package impact, and go/no-go.
After Harden analysis with immutable, handle-validated source snapshots #125 passes, create and approve one dependent implementation issue for immutable shared snapshots. That issue must name the selected platform contracts and limits, implement the source-acquisition seam with TDD, update user/developer documentation and changelog, preserve the compatibility list, cover ordinary analysis and explicitly assigned baseline write modes, validate a fresh installed wheel, dogfood the repository, and pass exact-head Windows/Ubuntu/macOS CI.
Do not begin production implementation from #125 and do not create the dependent issue until the prototype evidence has been independently reviewed and the product decisions above are approved.
Goal
Produce the bounded cross-platform design and prototype evidence required to eliminate selected-file replacement races in ordinary analysis. The target design is one handle-validated acquisition per applicable selected file and one immutable, invocation-owned byte snapshot consumed by every guard.
This issue is the design/prototype gate, not the production implementation. A dependent implementation issue may be authorized only after this issue chooses feasible Windows, Linux, and macOS contracts, resource limits, and public unavailable behavior.
Current behavior and reachable race
Scope resolution loads configuration, resolves explicit or Git-derived scope, canonicalizes candidates with
Path.resolve(), and freezes onlySelectedFile.reporting_pathplusSelectedFile.physical_pathinAnalysisContext. Explicit files outside the repository root are supported and use an absolute reporting identity. Recursive and Git selection omit selection-time symlink entries; all explicit, recursive,--changed-only,--staged, and--base-refmodes then converge on the same pathname-based guard API.Ordinary analysis later reopens selected pathnames independently:
errors="ignore"inguards/loc.py.analysis/regions.pythrough the syntax pipeline.markdown/scanner.py.Full ordinary analysis can therefore analyze two or three different objects or byte versions. LOC-only, syntax-only, and Markdown-only runs each retain the selection-to-open replacement race. The race applies equally to explicit, recursive, and Git selector modes and to selected in-root files; explicit outside-root files have the same replacement problem even though root-containment is not their authority.
When a LOC baseline is loaded,
run_analysisfreshly resolves and validates selected paths before guards run. That pathname check narrows stale selection but remains separated from every later open, so it does not close final-component or ancestor replacement. Syntax/provider unavailability is currently retained per file; LOC, Markdown, and ordinary filesystem read failures can abort the invocation. Baseline create/update are separate write modes that directly reread pathnames and do not run ordinary analysis.Threat model and severity
The race is technically reachable when a concurrent actor can rename, replace, or mutate a selected pathname or a relevant ancestor while Code Guard runs. Code Guard counts and parses repository content; it does not execute selected source.
Potential impact is outside-root derived-data disclosure through counts, callable identities, headings, locations, errors, or output; inconsistent findings; nondeterminism; denial of analysis; and changed aggregate or process exit state. Practical risk is low for a developer-controlled checkout, isolated CI, or a same-user actor with equivalent access. Risk is greater when a privileged scanner analyzes hostile or shared writable worktrees and publishes results across a privilege boundary.
This is defense-in-depth. It is not presently a release blocker or security advisory because no supported deployment establishes that concrete cross-privilege boundary. Reclassification requires such a deployment or a stronger demonstrated exploit; severity must not be raised merely because the race is technically reachable.
Exact security properties
The prototype must define and prove these terms before implementation:
"Secure open", "equivalent mechanism", and "fail closed" are not acceptance language. Every unsupported path/capability must map to a named observable result or an invocation error specified below; no pathname-only fallback is permitted.
Platform contract
The prototype must publish a Windows/Linux/macOS capability table covering supported Python 3.10-3.14 installations and the documented x86-64/ARM64 binary envelope. For each platform, record the exact APIs, flags, returned identity, path forms, error mapping, and deterministic tests.
POSIX investigation must cover:
os.open(..., dir_fd=...)traversal withO_DIRECTORYandO_NOFOLLOW, followed by a finalO_NOFOLLOWopen andfstatregular-file/identity checks;dir_fdsupport;Final-component-only
O_NOFOLLOWis insufficient./proc/self/fdis Linux-specific and must not be required on macOS. Linuxopenat2may be an optional implementation only; it is not a portable requirement and must not create a silent weaker fallback.Windows investigation must cover ordinary
open/os.openlimitations and a bounded handle-based prototype using appropriateCreateFileWreparse/share flags or another demonstrated API. Validation must operate on the opened handle and account for file/volume identity, junctions and other name-surrogate reparse points,GetFinalPathNameByHandleWnormalization, long paths, UNC paths, drive-letter casing, volume GUID/device aliases, and root/ancestor replacement. A string-prefix comparison is insufficient. The prototype must decide whether a small auditedctypesboundary, native helper, or dependency is necessary and prove packaging/architecture implications. A broad custom filesystem subsystem is a no-go without a separate product decision.A supported CI platform may not skip its promised containment test. If a primitive is unavailable on a platform inside the supported envelope, the prototype must choose an explicit analysis-unavailable or invocation-error contract, or recommend narrowing platform support; it must never silently downgrade validation.
Architecture and ownership boundary
Keep
SelectedFileresponsible for stable selection/reporting identity. The preferred model is a distinct frozen source object that references that identity and owns immutable bytes plus only the acquisition metadata needed for diagnostics. Acquisition belongs in a runner-owned source-acquisition phase after enabled-guard applicability is known, not in individual guards and not in scope discovery unless the prototype proves handles must be retained from that point.There must be one production source interface: LOC, syntax/Vue, Markdown, unavailable reporting, and future guards consume the captured source and never reopen
physical_path. Syntax regions receive the original bytes without a whole-file copy. LOC must preserve the current UTF-8errors="ignore", universal-newline, comment, and blank-line behavior. Markdown must preserve strict UTF-8 decoding and line semantics. Multiple guards for one file must be able to prove they consumed the identical byte object/value.Prefer lazy, memoized acquisition once per file in the union of enabled applicable guards, then release the snapshot after every applicable guard finishes. Do not eagerly capture excluded or inapplicable files. LOC-only execution must neither import nor instantiate Tree-sitter. If acquisition for one file fails, no guard may emit partial facts for that file, while completed independent files and guards remain available under the chosen public contract.
Reject dual production APIs in which any guard still accepts a selected pathname while another accepts a snapshot.
Mutation semantics
The prototype and dependent contract must specify these deterministic outcomes:
Do not rely on advisory locks. Windows sharing modes may reduce concurrent mutation but cannot revoke an already-open writer and must not be described as a complete guarantee.
Resource limits
The prototype must measure representative repository/file sizes and select a documented numeric default per-file byte limit and a bounded live aggregate-memory strategy before production implementation is authorized. It must show that lazy acquisition and prompt release do not undo #122's ordinary-analysis performance gains for repositories with thousands of selected files.
Capture must stream in bounded chunks and avoid duplicate whole-file decoded-text caches. Bytes may be shared directly; guard-specific decoding may be transient. An oversized file is a deterministic source-acquisition failure, never a partial finding or silent skip.
Temporary spooling is disallowed for the first implementation unless prototype evidence shows that a reasonable byte cap cannot preserve supported use. If later authorized, the design must require an OS temp location outside the repository, exclusive owner-only creation where supported, no pathname reopen by guards, read-only/immutable consumption, deterministic cleanup, and cleanup diagnostics that never replace the primary failure. No persistent cache metadata is allowed.
Compatibility requirements
Except for the explicitly approved acquisition-unavailable addition, preserve human output; full/debug/compact JSON; finding values and source locations; deterministic ordering; selected/analyzed/skipped/excluded counts; required policies; existing unavailable entries; aggregate states; exit codes;
INCOMPLETEbehavior under--ci; exclusions; loaded-baseline semantics; explicit and Git selectors; selection-time symlink behavior; thresholds and default guards; ordinary-analysis read-only behavior; and LOC-only parser dormancy.The prototype must specify the public shape and ordering of source-acquisition unavailability: its stable kind/message/path, which applicable guard IDs become incomplete, how scope counts change, and whether non-
--ciand--ciexits change. Completed evidence for other files/independent guards must be retained. This is an intentional public contract decision, not an implementation detail.Baseline create/update write modes are outside the ordinary-analysis implementation slice. The prototype must document their analogous pathname race and either place them explicitly in the dependent implementation issue or justify a later dependent issue; preserving baseline semantics does not imply that these write modes are already protected.
Any proposed change to outside-root support, mounts, reparse handling, scope counts, output, or exits must be called out for approval rather than hidden as hardening.
Deterministic validation
Use deterministic gates at the real production acquisition seam; no sleeps, retry races, wall-clock assertions, or exhaustive guard x selector x platform matrix.
The prototype must demonstrate:
Capability-local skips are allowed only outside a promised supported primitive. A skipped security guarantee on Windows, Ubuntu, or macOS CI is not a pass. Tests must assert shipped behavior, not mocks, wrappers, or helpers for their own sake.
Non-goals
Acceptance criteria
ctypes, a bounded helper, or a dependency and documents Python 3.10-3.14 plus x86-64/ARM64 packaging consequences.openat2and/proconly as optional/Linux-specific mechanisms.--ci/non-CI exits are specified with examples, including retained independent evidence.Release classification
Security hardening, currently non-blocking for the developer-controlled and isolated-CI threat model. #122 and the current release line remain unaffected unless a concrete supported cross-privilege deployment is established. This issue produces design evidence only and is not itself release work.
Implementation sequencing
Split delivery:
Do not begin production implementation from #125 and do not create the dependent issue until the prototype evidence has been independently reviewed and the product decisions above are approved.