Skip to content

security: separate logical and host path contracts - #17

Merged
stacknil merged 4 commits into
mainfrom
stacknil/path-contract-separation
Sep 11, 2026
Merged

stacknil merged 4 commits into
mainfrom
stacknil/path-contract-separation

Conversation

@stacknil

@stacknil stacknil commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

Separate Git logical path identity from portable filesystem materialization policy.

The snapshot reader now admits exact strict-UTF-8 logical paths without applying host-specific filename rules. The materializer independently performs complete-tree portable path validation and collision preflight before populating the target directory.

Motivation

The previous reader used a deliberately portable ASCII pathname subset. A real repository probe showed that this rejected eight legitimate current paths containing ordinary punctuation or Unicode characters.

Those names are valid snapshot identities and safe to treat as data. Host-specific pathname restrictions belong at the materialization boundary.

Reader contract

The reader now:

  • decodes Git tree components using strict UTF-8 and requires exact byte round-trip;
  • preserves exact decoded path identity without Unicode normalization or case folding;
  • accepts ordinary punctuation, Unicode, and host-specific filename strings as logical data;
  • continues to reject structural path invalidity, invalid encoding, controls, exact collisions, unsupported modes, and resource-limit violations.

SnapshotFile.path remains an exact Python str.

Materializer contract

Before creating any target-tree entry, the materializer performs complete-tree preflight covering:

  • portable host pathname restrictions;
  • reserved Windows names, including documented superscript device aliases;
  • .git consumer-policy rejection;
  • case aliases;
  • Unicode normalization aliases;
  • file/directory prefix collisions;
  • path and component limits;
  • safe component-wise path joining.

Arbitrary manually constructed Snapshot objects are validated independently rather than treated as trusted reader output.

Existing exclusive creation, regular-entry/reparse checks, no-follow verification, exact-byte readback, and cleanup behavior remain unchanged.

Cross-platform determinism

The canonical portable-v1 policy uses explicit project-defined collision rules rather than runtime-dependent ntpath.normcase() behavior.

For example:

A.txt / a.txt       collision
ẞ.txt / ß.txt       distinct
ß.txt / SS.txt      distinct
İ.txt / i.txt       distinct

Unicode normalization does not alter snapshot identity. Canonically equivalent NFC/NFD names remain distinct logical paths but are rejected as a portable materialization ambiguity.

Compatibility

The eight legitimate path classes that previously failed reader admission are now accepted without special-casing individual filenames.

A read-only full-repository probe verifies all 242 regular files through the reader and materializer with exact path identity, modes, blob identities, and file bytes preserved.

Security properties

Path-policy and collision failures occur before the materialized target tree is populated.

The change does not enable symlinks or gitlinks, execute repository content, or weaken existing object-identity and filesystem-write verification.

Scope

This change is independent from pull-head acquisition.

It does not modify:

Workflow activation remains a separate reviewed step after path admission is established.

Pipeline

#16 acquisition PR
    FROZEN / DRAFT

path-contract PR
    FINAL SECURITY AUDIT COMPLETE
    → READY FOR REVIEW
    → MERGE

workflow activation
    BLOCKED until both boundaries are stable

Validation

Windows 10.0.26200.9445, Python 3.14.3:

  • reader: 15/15 passed;
  • materializer: 24/24 passed;
  • integration: 13/13 passed;
  • full suite: 58/58 passed.

WSL Ubuntu 24.04.4 LTS, Python 3.12.3:

  • reader: 15/15 passed;
  • materializer: 24/24 passed;
  • integration: 13/13 passed;
  • full suite: 58/58 passed.

Additional validation:

  • pre-commit 4.5.1: passed;
  • Ruff 0.15.5: passed;
  • tag rendering: passed;
  • README rendering: passed;
  • 157-file Markdown validation: passed;
  • privacy/debug scans: passed;
  • git diff --check: passed, with only existing line-ending warnings;
  • exact-head GitHub Actions: Markdown Validation and Repo Sentinel Gate passed.

Read-only full-repository probe at commit 56fdb32012f287c8fc615df711c02d41a9919052, tree 960dc6c6496f1260f6fab74b64f26408024cd5fb:

  • 242/242 paths, modes, and blob OIDs preserved;
  • 2,260,062 bytes preserved exactly;
  • manifest SHA-256: ad39acf89d0826ce2651ed14d12e143d7f0a8b5cb6b2eb219b46895688f45d0a;
  • no repository content executed;
  • both cleanup boundaries succeeded.

@stacknil

Copy link
Copy Markdown
Owner Author

Finalizer review at 83b98d0 completed after CI. All seven current checks pass, including the changed-file blocking gate and synthetic PASS/FAIL/redaction contract. The initial gate failure was limited to entropy-like ASCII mapping literals; 83b98d0 replaces those literals with the equivalent explicit ASCII code-point range without changing the approved collision policy. Final diff review found no scope drift: acquisition, PR #16, workflow activation, baseline/scanner/report semantics, and repository settings remain unchanged. The fresh 242-file probe again preserved all paths, modes, blob identities, and 2,260,062 bytes with successful cleanup and no target-content execution. PR remains draft; workflow activation remains out of scope.

@stacknil
stacknil marked this pull request as ready for review September 11, 2026 04:09
@stacknil
stacknil merged commit d8e30ba into main Sep 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant