[GH-ISSUE #289] Use handle-based reparse-safe filesystem authority on Windows #161

Open
opened 2026-10-15 18:21:52 +00:00 by bsakaguchi · 5 comments
Owner

Originally created by @morluto on GitHub (Jul 16, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/289

Originally assigned to: @N0zoM1z0 on GitHub.

Problem Statement

Windows Ghidra P0 snapshots an approved target by pathname and verifies the copied bytes against the admitted SHA-256. This prevents changed bytes from producing Evidence, but it does not prove that every traversed path component remained inside approved authority or that reads, writes, and cleanup avoided reparse-point redirection.

Path canonicalization followed by a later pathname open leaves a time-of-check/time-of-use window. Checking only the final path also misses reparse points in parent components and does not provide stable file identity.

Purpose and Benefit

Make Windows filesystem authority follow verified handles and file identity rather than mutable path strings.

This would:

  • reject hostile symlink, junction, mount-point, and other reparse redirection before sensitive I/O;
  • bind authorization, snapshotting, digesting, and cleanup to the same opened object;
  • stop runtime cleanup from escaping through a replaced path component; and
  • provide evidence suitable for admitting hostile or mutable Windows targets in a future boundary.

Proposed Direction

Add a Windows handle-based path adapter under the provider-neutral filesystem boundary. It should:

  1. open each authority-relevant component with CreateFileW using FILE_FLAG_OPEN_REPARSE_POINT and, for directories, FILE_FLAG_BACKUP_SEMANTICS;
  2. inspect attributes and reparse tags and reject disallowed components;
  3. record stable volume and 128-bit file identity where supported;
  4. perform reads, copies, writes, and cleanup through retained or revalidated handles; and
  5. return canonical/final paths only as diagnostics, not as the authorization proof.

The first implementation should define exactly which filesystems and reparse tags are admitted. Unsupported SMB, ReFS, mount, or filter-driver semantics should fail closed rather than inherit NTFS assumptions.

Desired Outcome

  • Target admission and snapshot creation consume the already-authorized handle or an identity-checked duplicate, not a fresh pathname open.
  • Runtime-root creation, descriptor/log/endpoint writes, snapshot writes, and recursive cleanup cannot cross a disallowed reparse point.
  • Replacing a path component between admission and use either has no effect on the retained handle or produces a typed identity mismatch.
  • Diagnostics preserve requested path, final path, volume identity, file identity, and the rejected reparse tag where available.
  • Real Windows tests cover symlinks, junctions, mount points or documented unsupported equivalents, component replacement, cleanup escape attempts, NTFS, and each claimed additional filesystem.

Constraints and Non-goals

  • Depends on the native adapter decision in #287 and should integrate with the private runtime work rather than create a second filesystem backend.
  • Digest matching remains required; handle authority complements byte identity rather than replacing it.
  • Do not claim general extraction containment until every extraction path uses this boundary.
  • Path-prefix comparison and GetFinalPathNameByHandle alone are insufficient authority proofs.

Evidence and References

Originally created by @morluto on GitHub (Jul 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/289 Originally assigned to: @N0zoM1z0 on GitHub. ## Problem Statement Windows Ghidra P0 snapshots an approved target by pathname and verifies the copied bytes against the admitted SHA-256. This prevents changed bytes from producing Evidence, but it does not prove that every traversed path component remained inside approved authority or that reads, writes, and cleanup avoided reparse-point redirection. Path canonicalization followed by a later pathname open leaves a time-of-check/time-of-use window. Checking only the final path also misses reparse points in parent components and does not provide stable file identity. ## Purpose and Benefit Make Windows filesystem authority follow verified handles and file identity rather than mutable path strings. This would: - reject hostile symlink, junction, mount-point, and other reparse redirection before sensitive I/O; - bind authorization, snapshotting, digesting, and cleanup to the same opened object; - stop runtime cleanup from escaping through a replaced path component; and - provide evidence suitable for admitting hostile or mutable Windows targets in a future boundary. ## Proposed Direction Add a Windows handle-based path adapter under the provider-neutral filesystem boundary. It should: 1. open each authority-relevant component with CreateFileW using FILE_FLAG_OPEN_REPARSE_POINT and, for directories, FILE_FLAG_BACKUP_SEMANTICS; 2. inspect attributes and reparse tags and reject disallowed components; 3. record stable volume and 128-bit file identity where supported; 4. perform reads, copies, writes, and cleanup through retained or revalidated handles; and 5. return canonical/final paths only as diagnostics, not as the authorization proof. The first implementation should define exactly which filesystems and reparse tags are admitted. Unsupported SMB, ReFS, mount, or filter-driver semantics should fail closed rather than inherit NTFS assumptions. ## Desired Outcome - Target admission and snapshot creation consume the already-authorized handle or an identity-checked duplicate, not a fresh pathname open. - Runtime-root creation, descriptor/log/endpoint writes, snapshot writes, and recursive cleanup cannot cross a disallowed reparse point. - Replacing a path component between admission and use either has no effect on the retained handle or produces a typed identity mismatch. - Diagnostics preserve requested path, final path, volume identity, file identity, and the rejected reparse tag where available. - Real Windows tests cover symlinks, junctions, mount points or documented unsupported equivalents, component replacement, cleanup escape attempts, NTFS, and each claimed additional filesystem. ## Constraints and Non-goals - Depends on the native adapter decision in #287 and should integrate with the private runtime work rather than create a second filesystem backend. - Digest matching remains required; handle authority complements byte identity rather than replacing it. - Do not claim general extraction containment until every extraction path uses this boundary. - Path-prefix comparison and GetFinalPathNameByHandle alone are insufficient authority proofs. ## Evidence and References - Parent roadmap: #38 - Current implementation: src/ghidra/GhidraTargetSnapshot.ts, src/process/PrivateRuntimeRoot.ts, and the configured-root permission policy - CreateFileW reparse and directory-handle flags: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew - Reparse-point behavior: https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points-and-file-operations - Final-path diagnostics: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew - Stable file identity considerations: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
Author
Owner

@morluto commented on GitHub (Jul 17, 2026):

Status after #287 packaging research: this remains active and should define the shared Windows handle/file-identity primitives after the native package/loader/typed port is accepted. Reparse-safe traversal must operate on handles and return provider-neutral typed observations/errors rather than exposing raw Win32 handles. This issue should land before #290.

<!-- gh-comment-id:5001547181 --> @morluto commented on GitHub (Jul 17, 2026): Status after #287 packaging research: this remains active and should define the shared Windows handle/file-identity primitives after the native package/loader/typed port is accepted. Reparse-safe traversal must operate on handles and return provider-neutral typed observations/errors rather than exposing raw Win32 handles. This issue should land before #290.
Author
Owner

@morluto commented on GitHub (Jul 17, 2026):

A fail-closed prerequisite correction is now in the worktree: the system no-follow capability probe explicitly reports Windows reparse-safe handle admission unavailable even if a numeric POSIX O_NOFOLLOW constant is visible. Unit coverage proves a numeric flag cannot become Windows authority, and PrivateRuntimeRoot documentation now limits mode-0700 privacy claims to POSIX pending #290. Full npm run check passes. #289 remains open for the actual CreateFileW handle traversal, reparse-tag policy, stable file identity, and real Windows proof.

<!-- gh-comment-id:5002483970 --> @morluto commented on GitHub (Jul 17, 2026): A fail-closed prerequisite correction is now in the worktree: the system no-follow capability probe explicitly reports Windows reparse-safe handle admission unavailable even if a numeric POSIX O_NOFOLLOW constant is visible. Unit coverage proves a numeric flag cannot become Windows authority, and PrivateRuntimeRoot documentation now limits mode-0700 privacy claims to POSIX pending #290. Full npm run check passes. #289 remains open for the actual CreateFileW handle traversal, reparse-tag policy, stable file identity, and real Windows proof.
Author
Owner

@morluto commented on GitHub (Jul 17, 2026):

Additional dependency discovered while designing the optional native package loader: #289 must cover the native addon itself, not only Ghidra/JDK/target projection.

Hashing a .node pathname before require and rehashing afterward is not a load-authority proof. A replace/load/restore race can execute substituted native code while both reads match, and Node can return a previously cached native image for the same path. Post-load rejection is too late.

ADR-0004 now requires the addon to be projected into protected retained authority before native loading. The eventual process-global loader must reject a foreign pre-existing cache entry and bind its one accepted identity to that protected projection. I removed the unsafe pathname-loader prototype rather than preserving a misleading gate. This adds an explicit #289 acceptance dependency without changing the current fail-closed capability report. Full npm run check passes.

<!-- gh-comment-id:5004315834 --> @morluto commented on GitHub (Jul 17, 2026): Additional dependency discovered while designing the optional native package loader: #289 must cover the native addon itself, not only Ghidra/JDK/target projection. Hashing a .node pathname before require and rehashing afterward is not a load-authority proof. A replace/load/restore race can execute substituted native code while both reads match, and Node can return a previously cached native image for the same path. Post-load rejection is too late. ADR-0004 now requires the addon to be projected into protected retained authority before native loading. The eventual process-global loader must reject a foreign pre-existing cache entry and bind its one accepted identity to that protected projection. I removed the unsafe pathname-loader prototype rather than preserving a misleading gate. This adds an explicit #289 acceptance dependency without changing the current fail-closed capability report. Full npm run check passes.
Author
Owner

@morluto commented on GitHub (Jul 17, 2026):

Current-main implementation update (2026-07-17):

The provider-neutral #289 port is now stricter without advertising Windows authority:

  • native admission observations cross the boundary as unknown and are parsed with a strict schema;
  • the reported filesystem is limited to NTFS;
  • volume identity is canonical lowercase 16-hex FILE_ID_INFO VolumeSerialNumber, and file identity is lowercase hex of Identifier[0..15] in byte-array order;
  • stable identity is derived by the application and explicitly scoped to the retained handle/admitted volume, not treated as a durable global authorization key;
  • requested-path mismatches, uppercase/non-canonical identities, unsupported filesystems, unknown fields, and malformed observations return native_contract_mismatch and poison/close the returned token;
  • alias poisoning now uses the originally registered resource cleanup, respects active projection/spawn leases, and converts synchronous throws or rejected cleanup promises into a terminal typed native_failure rather than hanging teardown.

The ADR now states the proof boundary precisely: TypeScript validates the shape and canonical encoding of reported observations; only the trusted native implementation plus real-Windows verification can prove CreateFileW traversal, filesystem provenance, and GetFileInformationByHandleEx(FileIdInfo) provenance.

Independent review found and drove fixes for lease-bypassing alias cleanup, provenance overclaiming, underspecified FILE_ID_INFO encoding, and rejected-cleanup hangs. The exact final tree passes npm run check: 160 test files, 1,063 tests.

This issue remains open. The native Node-API implementation, protected addon projection/load binding, handle-relative I/O, reparse-tag rejection, and real Windows NTFS adversarial verifier are still required before capability availability or closure.

<!-- gh-comment-id:5004723511 --> @morluto commented on GitHub (Jul 17, 2026): Current-main implementation update (2026-07-17): The provider-neutral #289 port is now stricter without advertising Windows authority: - native admission observations cross the boundary as unknown and are parsed with a strict schema; - the reported filesystem is limited to NTFS; - volume identity is canonical lowercase 16-hex FILE_ID_INFO VolumeSerialNumber, and file identity is lowercase hex of Identifier[0..15] in byte-array order; - stable identity is derived by the application and explicitly scoped to the retained handle/admitted volume, not treated as a durable global authorization key; - requested-path mismatches, uppercase/non-canonical identities, unsupported filesystems, unknown fields, and malformed observations return native_contract_mismatch and poison/close the returned token; - alias poisoning now uses the originally registered resource cleanup, respects active projection/spawn leases, and converts synchronous throws or rejected cleanup promises into a terminal typed native_failure rather than hanging teardown. The ADR now states the proof boundary precisely: TypeScript validates the shape and canonical encoding of reported observations; only the trusted native implementation plus real-Windows verification can prove CreateFileW traversal, filesystem provenance, and GetFileInformationByHandleEx(FileIdInfo) provenance. Independent review found and drove fixes for lease-bypassing alias cleanup, provenance overclaiming, underspecified FILE_ID_INFO encoding, and rejected-cleanup hangs. The exact final tree passes npm run check: 160 test files, 1,063 tests. This issue remains open. The native Node-API implementation, protected addon projection/load binding, handle-relative I/O, reparse-tag rejection, and real Windows NTFS adversarial verifier are still required before capability availability or closure.
Author
Owner

@morluto commented on GitHub (Jul 20, 2026):

Split into focused child issues:

These replace the broad remaining scope in this issue. Closing this parent can happen once all children are closed.

<!-- gh-comment-id:5023380639 --> @morluto commented on GitHub (Jul 20, 2026): Split into focused child issues: - [ ] #324 - [ ] #325 - [ ] #326 These replace the broad remaining scope in this issue. Closing this parent can happen once all children are closed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#161
No description provided.