Skip to content

Temp retention and deletion-handle contract

This packet records the bounded deterministic fixture and contract work in issue #30. PR #42 merged as 3d07e9ccbfd441c388b1a3a69edc92fc326a4a66 and closed the issue. The runtime already used native same-handle deletion before this packet; the merged work adds evidence and states its concurrency limits. It does not enable stable status or replace the deferred Windows client/server, actual-root, ReFS, filesystem-filter, or hostile-driver labs. See the release ledger for current acceptance gates.

Retention decision

Both temp commands discover a candidate's volume/file identity and use a single inclusive UTC cutoff. After the command's ShouldProcess approval, path/type and ancestry checks, they open that path using OpenForDeletion. Acquisition requests DELETE | FILE_READ_ATTRIBUTES, shares only FILE_SHARE_READ, opens the reparse point itself, and does not schedule deletion. It adds no file-content read permission; the independent permissions evidence belongs to issue #29.

Each command passes the resulting $CurrentHandle to ReadIdentity, rejects a directory, reparse point, missing identity, different volume/file identity, or a last-write time newer than the plan cutoff, and passes that same handle to MarkForDeletion. The length used for BytesReclaimed also comes from this handle, not discovery or a second pathname lookup. Every exit disposes the candidate handle; the command's outer finally releases retained directory handles.

Retention is defined by the last-write metadata observed from the deletion handle. It is not an atomic comparison-and-delete operation. The following contract is intentional and tested:

Change Result
Recent or different old regular file replaces the approved object before acquisition Its file identity differs, so it is skipped and contributes no removed count or bytes. Keeping the original object alive in fixtures avoids accidental file-ID reuse.
The same object becomes recent before handle metadata is read It is skipped, including when content was also changed.
The same object remains eligible, including exactly at the cutoff It may be removed. Bytes are its current observed logical length; stale discovery length is not used.
A data writer is already open, even when that writer shares deletion Deletion-handle acquisition fails with a sharing violation. The result records a file failure and emits TempFileRemovalFailed; -ErrorAction Stop terminates and releases plan handles.
A new ordinary data writer or rename is attempted while the deletion handle is held Windows sharing rules block it. This includes access through other hard-link names for the same object.
An attribute-only writer updates last-write time while the handle is held Windows can allow it. A later handle metadata read sees the update, but an update after the command's read does not revoke its earlier eligibility decision.
The approved path disappears or its file is renamed before acquisition The command skips the missing name and does not chase the object to its new name.
The approved name is relinked to the same approved object Identity still matches. Removing that name preserves the other hard link; logical bytes counted are not proof of physically freed space.
Directory or reparse substitution It is preserved. Path preflight and same-handle type/reparse checks remain in place. Failures to acquire or inspect are never treated as successful removal.

The native regression intentionally updates the timestamp after observing an old timestamp, then requests disposition on the held fixture handle. Deletion succeeds: FILE_SHARE_READ does not make timestamps immutable. A second timestamp check would only move this race; it would not create an atomic predicate. Microsoft documents the access/share rules in CreateFile and the distinct FILE_WRITE_ATTRIBUTES requirement in SetFileTime.

Threat model and limits

These guards address ordinary user-mode path replacement, rename, traversal and handle-sharing races on a Windows filesystem that provides the required stable volume/file identities. Retained ancestor handles prevent a checked parent from being renamed during mutation; its separate contract and fixtures are tracked in issue #28. The final component is opened with FILE_FLAG_OPEN_REPARSE_POINT and its handle attributes are checked. No path-based deletion fallback is permitted.

An actor able to set timestamps can deliberately backdate content before the observation or change the timestamp afterward using attribute-only access. The cleaner is not a hostile-writer transaction system. File-ID recycling after an object is destroyed, filesystem filters, kernel/administrator interference, pre-existing writable mappings, unusual delayed timestamp behavior, and filesystems without the expected identity/sharing semantics are outside this bounded guarantee. No test here establishes atomic metadata snapshots across the separate native information queries or atomicity between metadata and disposition.

Byte totals describe logical lengths for successfully reconciled removed names. Hard links, allocation, and concurrent deletion-pending behavior can make actual free-space changes differ. Existing post-close failure reconciliation remains in place; DELETE-PENDING, ROOT-RACE and HANDLE-RECOVERY hostile drivers remain deferred.

Deterministic acceptance evidence

The new TempRetentionHandle.Tests.ps1 suite inserts a synchronous mutation at the last path resolver return before native acquisition. Discovery, approval and path preflight have already occurred; the public command and native deletion implementation remain unmodified. The tests assert that the barrier ran exactly once. This avoids timing sleeps, probabilistic races, production instrumentation, and replacing native deletion with a mock. Windows-root resolution is mocked to the disposable fixture root; environment variables and held writers are restored after every test. Junctions are removed as links before fixture teardown.

Issue criterion Evidence
Recent and different old regular replacement Both commands reject each replacement at the acquisition barrier, preserve the original renamed object and replacement bytes, and reconcile skips/bytes.
Same-handle age, identity and type; acquisition without disposition Public command/native call-chain review; native open/read/close preserves bytes; replacement, timestamp and current-length behavioral cases fail if stale discovery values authorize removal. Existing TempCandidateSafety.Tests.ps1 covers native file and directory rename denial while held.
Concurrent metadata/content updates Both commands test same-object timestamp/content updates before acquisition and the inclusive cutoff/current length. Native tests verify data-writer denial, allowed attribute-only writes, fresh handle observations, and the non-atomic disposition limit.
Disappearance, directory/reparse swaps, renamed paths and hard links New acquisition-barrier renamed-path, directory/junction substitution and same-object relink fixtures for both commands; existing TempCandidateSafety.Tests.ps1 and IdentitySafety.Tests.ps1 retain disappearance and link guards. Junction target contents are checked; this is not a privileged file-symbolic-link or filesystem-filter lab.
Sharing errors, Stop, counters and bytes Both commands verify late writer failure with native error 32, stable error ID/category, zero removals/bytes, and Stop unwinding with a successful root rename afterward. Access-mask/ACL cases remain independently evidenced in issue #29.
Approval and permissions boundaries Both commands prove WhatIf never reaches acquisition. Existing Confirm tests and the native no-content-read access mask are unchanged.

Historical local checkpoint

Local evidence is recorded against base af330858b41335366476f0b845e3f73a3c1497d1 plus the uncommitted new test and this document, using isolated Pester 5.7.1. It is draft fixture evidence, not an exact merged-commit or hosted-matrix claim. The native helper is explicitly initialized before Pester in the Windows PowerShell 5.1 process. Both runtimes are launched with -NoProfile -NonInteractive.

Runtime Total Passed Failed Skipped Not run
PowerShell 7.6.6 28 28 0 0 0
Windows PowerShell 5.1.26100.9444 28 28 0 0 0

The tested new file's SHA-256 is a8d0a1be296e8618ae7ce47174218b777506913b9711d88381529c9af887d429. git diff --check and the new files' whitespace checks passed. No runtime source behavior was changed; the integration also clarifies comment-based help. This local checkpoint does not claim execution on PowerShell 7.4 or 7.5; the later exact-head and merged-source hosted results are recorded in the release ledger.

The retained machine-readable reports are src/Reports/issue30/ps7-final.xml, ps7-final.json, ps51-final.xml, and ps51-final.json (ignored local evidence). The later final-head and merged-source evidence was recorded in the release ledger. Command stability still requires the applicable product and release gates.

Committed integration checkpoint

Exact clean commit 1f6a28e1da328440f1942c66648e9991ba3c5522 passed the 28 new cases plus four documentation-contract tests: 32/32 on PowerShell 7.6.6 and Windows PowerShell 5.1.26100.9444, using Pester 5.7.1 on Windows 10.0.26200.0. Both runs had zero failures, skips and not-run tests. The new test hash remained the value above. The strict clean Zensical 0.0.62 build and configuration validation passed. This is local exact-commit evidence; the PR's later final-head hosted matrix and merge results are recorded in the release ledger; this checkpoint remains local evidence only.