From 28d5c397c83f901e4086083868041711371ad32a Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:33:01 +0800 Subject: [PATCH] fix(windows): allow Java to read retained target snapshots (#1027) --- native/windows/src/filesystem.cc | 32 ++++++++++++++++++++++++++----- scripts/verify-windows-native.mjs | 23 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc index 758075f9f..8d7ef7a6b 100644 --- a/native/windows/src/filesystem.cc +++ b/native/windows/src/filesystem.cc @@ -277,14 +277,15 @@ static void directory(Runtime& root, const std::wstring& relative) { } } -static Handle createPrivateFile(Runtime& root, const std::wstring& relative) { +static Handle createPrivateFile(Runtime& root, const std::wstring& relative, + DWORD access = DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL) { const auto path = relativePath(root, relative); const auto parentPath = path.substr(0, path.find_last_of(L'\\')); auto parent = runtimeDirectory(root, parentPath); verifyPrivate(parent->get(), parentPath, parentPath == root.path); Security security; auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1), - DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path); + access, false, security, path); verifyPrivate(file.get(), path, false); return file; } @@ -305,6 +306,26 @@ static void dispose(HANDLE handle, const std::wstring& path) { require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path); } +static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& path) { + // Snapshot readers do not share DELETE access. Release the writer only for + // failed-copy cleanup, with its parent pinned and its identity recorded. + auto parent = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\'))); + FILE_ID_INFO expected{}; + require(GetFileInformationByHandleEx(file.get(), FileIdInfo, &expected, sizeof(expected)), + "Read incomplete snapshot identity failed", path); + file.reset(); + auto cleanup = openComponent(path, DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES, false); + FILE_ID_INFO observed{}; + require(GetFileInformationByHandleEx(cleanup.get(), FileIdInfo, &observed, sizeof(observed)), + "Read snapshot cleanup identity failed", path); + require(expected.VolumeSerialNumber == observed.VolumeSerialNumber && + std::equal(std::begin(expected.FileId.Identifier), std::end(expected.FileId.Identifier), + std::begin(observed.FileId.Identifier)), + "Incomplete snapshot identity changed before cleanup", path, ERROR_ACCESS_DENIED); + verifyPrivate(cleanup.get(), path, false); + dispose(cleanup.get(), path); +} + void closeRuntime(Runtime& root) { if (root.closed) return; // Windows keeps the bearer descriptor and snapshot leases until this owner @@ -414,8 +435,7 @@ static void completeSnapshot(napi_env env, napi_status status, void* pointer) { work->root->snapshotPending = false; const auto reject = [&](const Failure& failure) { try { - dispose(work->destination.get(), work->path); - work->destination.reset(); + disposeSnapshot(*work->root, work->destination, work->path); napi_reject_deferred(env, work->deferred, failureValue(env, failure)); } catch (const Failure& cleanupFailure) { const Failure combined( @@ -447,7 +467,9 @@ static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root, auto work = std::make_unique(env, root); work->source = openFile(sourcePath); work->path = relativePath(root, relative); - work->destination = createPrivateFile(root, relative); + // Retain the write lease with read-only sharing, but omit DELETE authority: + // Java's RandomAccessFile reader shares read/write, not delete access. + work->destination = createPrivateFile(root, relative, GENERIC_WRITE | GENERIC_READ | READ_CONTROL); napi_value promise; check(napi_create_promise(env, &work->deferred, &promise)); check(napi_create_reference(env, rootValue, 1, &work->rootReference)); diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs index 0e5375806..7b95c8e4a 100644 --- a/scripts/verify-windows-native.mjs +++ b/scripts/verify-windows-native.mjs @@ -300,6 +300,28 @@ try { readbackRuntime.observation.path, "snapshot.bin", ); + // Java RandomAccessFile uses read/write sharing without delete sharing. + // Exercise that independent Win32 reader contract without requiring Java. + const readerDigest = execFileSync( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + "$ErrorActionPreference = 'Stop'; " + + "$stream = [IO.File]::Open($env:REA_SNAPSHOT_READER_PATH, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite); " + + "$hash = [Security.Cryptography.SHA256]::Create(); " + + "try { [BitConverter]::ToString($hash.ComputeHash($stream)).Replace('-', '').ToLowerInvariant() } " + + "finally { $hash.Dispose(); $stream.Dispose() }", + ], + { + encoding: "utf8", + timeout: 15_000, + env: { ...process.env, REA_SNAPSHOT_READER_PATH: snapshotPath }, + }, + ).trim(); + assert.equal(readerDigest, readbackSnapshot.sha256); + await assert.rejects(writeFile(snapshotPath, "replace")); await assert.rejects(rename(snapshotPath, `${snapshotPath}.renamed`)); await assert.rejects(rm(snapshotPath)); assert.equal( @@ -310,6 +332,7 @@ try { ...report.controls, completedRuntimeFileReadback: true, completedRuntimeFileMutationDenied: true, + snapshotReadWriteSharingReader: true, }; } finally { await readbackRuntime.close();