fix(windows): retain failed native inspection ownership (#1826)

This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-11 21:33:25 +08:00
1 parent 6ac5c36503
commit 0b4ce83618
11 files changed
+240 -19

No files matched your search

+24
View File
@@ -106,6 +106,30 @@ npm run verify:windows-native
npm run verify:ghidra:windows
```
The native inspection probe retains its directory owner if deletion fails. A
later availability query retries that cleanup before creating another probe;
successful admission and static artifact failures remain cached. Environment
teardown makes one final cleanup attempt and reports a persistent failure to
stderr rather than claiming the directory was removed.
CI separately builds and verifies a test-only artifact:
```sh
node scripts/build-windows-native.mjs --inspection-fixture
```
On Windows, after compiling the runtime:
```powershell
node scripts/verify-windows-native-inspection.mjs
```
This artifact injects one access-denied disposition result while using a real
NTFS directory. The verifier checks retained ownership, recovery on a later
query, cached successful admission, and cleanup on environment teardown. It
does not reproduce or identify the host-dependent cause reported in #1701.
The fixture is stored under `.cache/` and is excluded from the shipped addon.
The native lane checks DACL readback independently, source write/replacement
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
asynchronous cancellation, cleanup containment,
+37 -3
View File
@@ -1,6 +1,7 @@
#include "authority.hpp"
#include <delayimp.h>
#include <cstdio>
namespace rea {
@@ -89,17 +90,45 @@ napi_value failureValue(napi_env env, const Failure& failure) {
return error;
}
struct InspectionContext { std::unique_ptr<Runtime> probe; };
static void cleanupInspection(void* data) noexcept {
std::unique_ptr<InspectionContext> context(static_cast<InspectionContext*>(data));
if (!context->probe) return;
try { closeRuntime(*context->probe); }
catch (const Failure& failure) {
// Environment teardown cannot retain an owner or return an error. Report
// terminal cleanup failure instead of claiming the directory was removed.
std::fprintf(stderr, "REA Windows probe cleanup failed (Win32 %lu) path=\"",
static_cast<unsigned long>(failure.win32));
// Emit reversible UTF-16 escapes without allocation or CRT locale conversion.
for (wchar_t unit : failure.path) {
if (unit == L'"' || unit == L'\\') std::fputc('\\', stderr);
if (unit >= 0x20 && unit <= 0x7e) std::fputc(static_cast<int>(unit), stderr);
else std::fprintf(stderr, "\\u%04x", static_cast<unsigned>(unit));
}
std::fprintf(stderr, "\"\n");
}
catch (const std::exception& failure) {
std::fprintf(stderr, "REA Windows probe cleanup failed: %s\n", failure.what());
}
catch (...) {
std::fprintf(stderr, "REA Windows probe cleanup failed: unknown native exception\n");
}
}
static napi_value call(napi_env env, napi_callback_info info) {
try {
std::array<napi_value, 2> args;
size_t count = args.size();
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, nullptr));
void* data = nullptr;
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, &data));
require(count == 2, "Native call requires operation and arguments", L"", ERROR_INVALID_PARAMETER);
const auto operation = wide(env, args[0]);
const auto values = array(env, args[1]);
if (operation == L"inspect") {
require(values.empty(), "Inspect takes no arguments", L"", ERROR_INVALID_PARAMETER);
return inspect(env);
return inspect(env, static_cast<InspectionContext*>(data)->probe);
}
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
return filesystemCall(env, operation, values);
@@ -114,8 +143,13 @@ static napi_value call(napi_env env, napi_callback_info info) {
}
static napi_value initialize(napi_env env, napi_value exports) {
auto context = std::make_unique<InspectionContext>();
if (napi_add_env_cleanup_hook(env, cleanupInspection, context.get()) != napi_ok) return nullptr;
// The environment owns callback data before a function can expose it. Failed
// initialization still leaves a valid context for the registered cleanup hook.
auto* data = context.release();
napi_value function;
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, data, &function) != napi_ok) return nullptr;
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
return exports;
}
+1 -1
View File
@@ -132,6 +132,6 @@ std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::ws
bool protocolStdin = false, ULONGLONG memoryLimit = 0);
napi_value processCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
napi_value inspect(napi_env env);
napi_value inspect(napi_env env, std::unique_ptr<Runtime>& probe);
} // namespace rea
+8 -4
View File
@@ -412,13 +412,17 @@ napi_value processCall(napi_env env, const std::wstring& operation, const std::v
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
}
napi_value inspect(napi_env env) {
napi_value inspect(napi_env env, std::unique_ptr<Runtime>& probe) {
// Settle the previous owner before admitting another probe. Failed cleanup
// retains its handles, including when identity construction itself failed.
if (probe) { closeRuntime(*probe); probe.reset(); }
wchar_t temp[32768];
const auto length = GetTempPathW(32768, temp);
require(length > 0 && length < 32768, "Read temporary directory coordinate failed");
auto root = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
auto filesystem = identity(env, root->directory.get(), root->path);
closeRuntime(*root);
probe = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
auto filesystem = identity(env, probe->directory.get(), probe->path);
closeRuntime(*probe);
probe.reset();
auto job = createJob();
Attributes attributes(1);
HANDLE raw = job.get();