[GH-ISSUE #8975] [Bug] fix(filesystem): Windows ChangeTime drift reports freshly written executables as changed #7872

Open
opened 2026-11-05 06:45:38 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @wouter-bon on GitHub (Nov 5, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8975

Area: Evidence, artifacts, or reconstruction

Observed behavior

On Windows, REA sometimes refuses a freshly written executable and reports that it changed while being read, although nothing modified it. Examples:

  • Root artifact changed before inventory: ...\app from src/artifacts/inventory/classify.ts:135
  • Selected input changed while being read: ...\selected from src/filesystem/RegularFile.ts via RegularFileChangedError

Windows updates NTFS ChangeTime (ctime) on the untouched file shortly after it is written. REA's freshness checks compare ctime, so they classify the file as changed.

Reproduction

Observed: a standalone Node probe on Windows 11, outside REA. It writes a copy of C:\Windows\System32\where.exe to a new temp directory as app, opens it, takes fstat, then polls stat for 1.5 s. Out of 30 runs:

  • In 29, ctimeMs changed between 0.3 and 1.0 s after the write.
  • dev, ino, size, mode and mtimeMs stayed the same in every run.

fsutil file queryEA on such a copy afterwards lists the extended attributes $KERNEL.PURGE.SEC.FILEHASH and $CI.CATALOGHINT. The probe script is available on request.

Observed in the test suite: npm run test:fast on Windows. The failures were intermittent: each test failed in some runs and passed in others.

  • src/artifacts/pyinstaller/PyInstallerAdmission.test.ts › "retains pe carrier dialect at offset 8190 through member projection"
  • src/electron/ElectronFuseArchitecture.test.ts › "preserves PE machine 450 without assuming managed OS overrides" and "reads PE machine facts independently of optional-header validity"
  • src/application/environment/environmentExport.test.ts › "copies the package tarball beside the Dockerfile and reports it as the package source". This is a .tgz, not an executable. I did not confirm that ctime is the field that changed here.

Public CLI/MCP reproduction: NOT_ENOUGH_INFO. I did not run a freshly downloaded executable through the CLI or MCP. The source path suggests that inspecting a file within about a second of writing it can fail the same way.

Expected behavior

A file whose content and identity are unchanged should be admitted. Freshness checks should still detect real rewrites, including a same-size rewrite with a restored mtime (src/process/capture/FilesystemSnapshot.test.ts › "rejects a same-size rewrite even when its modification time is restored").

Impact and scope

  • Platform: Windows only. On Linux and macOS, ctime does not change without a metadata change.

  • Likely affected: fresh downloads, build outputs and extracted executables inspected right after they are written. The failure stops once the scan has finished, and repeating the request usually succeeds.

  • Comparison sites (source only):

    • src/filesystem/RegularFile.ts (sameRegularFileState)
    • src/artifacts/ArtifactReader.ts (sameArtifactSourceIdentity)
    • src/sqlite/SqliteDatabaseSnapshot.ts
    • src/reference/ReferenceSourceReaderValidate.ts
    • src/process/capture/ProcessCaptureLifecycle.ts

    src/native/SignatureTargetBinding.ts also compares ctime, but it is macOS-only.

REA version

6.8.0, source checkout at 71f680d7c.

Runtime and platform

  • Windows 11 Pro 10.0.26220, NTFS
  • Node.js 24.22.1

Interface

Other: found through unit tests; the CLI/MCP path was not run.

Earliest failure stage

Artifact or evidence registration

Analysis or suspected cause

Suspected cause: Windows security components add the extended attributes listed above to newly written executables asynchronously, and that moves ChangeTime. I inferred this from the attribute names and timing; I have not confirmed it from Microsoft documentation.

Rejected fix: ignoring ctime on Windows. I tried it locally and reverted it. It made FilesystemSnapshot.test.ts › "rejects a same-size rewrite even when its modification time is restored" fail. Resetting mtime with utimes is easy, and on Windows only ctime catches that rewrite. Ordinary tools cannot set ChangeTime.

Possible directions (proposals, not tested):

  • When only ctime differs on Windows, re-verify the content (for example the digest captured at admission) before reporting a change. This applies only where a digest or bytes are already available.
  • Settle the first stat: on Windows, when a file's ctime is under about 2 s old, wait until it stops changing before recording the snapshot. Source-only check: about 112 source files take their own stat/lstat snapshots, often before they open the file (for example src/electron/ElectronFusePath.ts and the root check in src/artifacts/inventory/classify.ts), and there is no shared input boundary. Doing this only in openRegularFile would cover some paths, not all.
  • Retry once at the operation level on Windows after an "artifact changed" failure. This is one central change, but it only fits operations without side effects.

Each option needs a maintainer decision on the integrity trade-off.

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.
Originally created by @wouter-bon on GitHub (Nov 5, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8975 **Area:** Evidence, artifacts, or reconstruction ### Observed behavior On Windows, REA sometimes refuses a freshly written executable and reports that it changed while being read, although nothing modified it. Examples: - `Root artifact changed before inventory: ...\app` from `src/artifacts/inventory/classify.ts:135` - `Selected input changed while being read: ...\selected` from `src/filesystem/RegularFile.ts` via `RegularFileChangedError` Windows updates NTFS ChangeTime (`ctime`) on the untouched file shortly after it is written. REA's freshness checks compare `ctime`, so they classify the file as changed. ### Reproduction **Observed:** a standalone Node probe on Windows 11, outside REA. It writes a copy of `C:\Windows\System32\where.exe` to a new temp directory as `app`, opens it, takes `fstat`, then polls `stat` for 1.5 s. Out of 30 runs: - In 29, `ctimeMs` changed between 0.3 and 1.0 s after the write. - `dev`, `ino`, `size`, `mode` and `mtimeMs` stayed the same in every run. `fsutil file queryEA` on such a copy afterwards lists the extended attributes `$KERNEL.PURGE.SEC.FILEHASH` and `$CI.CATALOGHINT`. The probe script is available on request. **Observed in the test suite:** `npm run test:fast` on Windows. The failures were intermittent: each test failed in some runs and passed in others. - `src/artifacts/pyinstaller/PyInstallerAdmission.test.ts` › "retains pe carrier dialect at offset 8190 through member projection" - `src/electron/ElectronFuseArchitecture.test.ts` › "preserves PE machine 450 without assuming managed OS overrides" and "reads PE machine facts independently of optional-header validity" - `src/application/environment/environmentExport.test.ts` › "copies the package tarball beside the Dockerfile and reports it as the package source". This is a `.tgz`, not an executable. I did not confirm that ctime is the field that changed here. **Public CLI/MCP reproduction:** NOT_ENOUGH_INFO. I did not run a freshly downloaded executable through the CLI or MCP. The source path suggests that inspecting a file within about a second of writing it can fail the same way. ### Expected behavior A file whose content and identity are unchanged should be admitted. Freshness checks should still detect real rewrites, including a same-size rewrite with a restored mtime (`src/process/capture/FilesystemSnapshot.test.ts` › "rejects a same-size rewrite even when its modification time is restored"). ### Impact and scope - **Platform:** Windows only. On Linux and macOS, ctime does not change without a metadata change. - **Likely affected:** fresh downloads, build outputs and extracted executables inspected right after they are written. The failure stops once the scan has finished, and repeating the request usually succeeds. - **Comparison sites (source only):** - `src/filesystem/RegularFile.ts` (`sameRegularFileState`) - `src/artifacts/ArtifactReader.ts` (`sameArtifactSourceIdentity`) - `src/sqlite/SqliteDatabaseSnapshot.ts` - `src/reference/ReferenceSourceReaderValidate.ts` - `src/process/capture/ProcessCaptureLifecycle.ts` `src/native/SignatureTargetBinding.ts` also compares ctime, but it is macOS-only. ### REA version 6.8.0, source checkout at `71f680d7c`. ### Runtime and platform - Windows 11 Pro 10.0.26220, NTFS - Node.js 24.22.1 ### Interface Other: found through unit tests; the CLI/MCP path was not run. ### Earliest failure stage Artifact or evidence registration ### Analysis or suspected cause **Suspected cause:** Windows security components add the extended attributes listed above to newly written executables asynchronously, and that moves ChangeTime. I inferred this from the attribute names and timing; I have not confirmed it from Microsoft documentation. **Rejected fix:** ignoring ctime on Windows. I tried it locally and reverted it. It made `FilesystemSnapshot.test.ts` › "rejects a same-size rewrite even when its modification time is restored" fail. Resetting mtime with `utimes` is easy, and on Windows only ctime catches that rewrite. Ordinary tools cannot set ChangeTime. **Possible directions (proposals, not tested):** - When only ctime differs on Windows, re-verify the content (for example the digest captured at admission) before reporting a change. This applies only where a digest or bytes are already available. - Settle the first stat: on Windows, when a file's ctime is under about 2 s old, wait until it stops changing before recording the snapshot. Source-only check: about 112 source files take their own `stat`/`lstat` snapshots, often before they open the file (for example `src/electron/ElectronFusePath.ts` and the root check in `src/artifacts/inventory/classify.ts`), and there is no shared input boundary. Doing this only in `openRegularFile` would cover some paths, not all. - Retry once at the operation level on Windows after an "artifact changed" failure. This is one central change, but it only fits operations without side effects. Each option needs a maintainer decision on the integrity trade-off. ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data.
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#7872
No description provided.