diff --git a/src/guarded-mutation.ts b/src/guarded-mutation.ts index f38e193..c3ab40a 100644 --- a/src/guarded-mutation.ts +++ b/src/guarded-mutation.ts @@ -32,6 +32,9 @@ export async function withAsyncDirectoryGuards( } catch (error) { if (options.onPostGuardFailure) { try { + // The mutation may have returned an owned resource before the post-guard + // check detected a swapped directory. Give callers one chance to close + // handles without letting cleanup hide the boundary failure. await options.onPostGuardFailure(result, error); } catch { // Preserve the boundary failure. Cleanup is best-effort. diff --git a/src/pinned-write.ts b/src/pinned-write.ts index 4cde454..258a838 100644 --- a/src/pinned-write.ts +++ b/src/pinned-write.ts @@ -217,6 +217,8 @@ async function runPinnedWriteFallback(params: { ), { onPostGuardFailure: async (openedHandle) => { + // The parent failed verification, so targetPath may now resolve + // somewhere else. Close the fd, but do not clean up by path. await openedHandle.close().catch(() => undefined); }, }, diff --git a/src/root-impl.ts b/src/root-impl.ts index efcc7e8..27ce272 100644 --- a/src/root-impl.ts +++ b/src/root-impl.ts @@ -1627,7 +1627,7 @@ async function writeMissingFileFallback( }, { onPostGuardFailure: async ({ handle }) => { - created = false; + created = false; // Parent is untrusted now; skip outer path cleanup by name. await handle.close().catch(() => undefined); }, }, diff --git a/src/safe-path-segment.ts b/src/safe-path-segment.ts index 30176bd..8a9aa77 100644 --- a/src/safe-path-segment.ts +++ b/src/safe-path-segment.ts @@ -30,6 +30,8 @@ export function assertSafePathSegment( segment: string, options: SafePathSegmentOptions = {}, ): string { + // Validate the exact value callers will later join into paths; trimming here + // would let whitespace-padded ids pass and then be used verbatim. if (!isSafePathSegment(segment, options)) { throw new FsSafeError( "invalid-path",