mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
feat(ghidra): enable Windows x64 read-only analysis (#699)
* feat(ghidra): enable Windows x64 read-only analysis * test(ghidra): exercise native Windows runtime boundaries * test(ghidra): verify current Windows inventory and isolate ACL observer * fix(ghidra): align Windows admission and address P0 review
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# Windows native controls
|
||||
|
||||
REA ships one Windows x64 Node-API 8 addon. Users do not compile it or install
|
||||
another runtime. Ghidra and a full JDK remain bring-your-own prerequisites.
|
||||
The addon uses Windows 10+ APIs and admits fixed local NTFS volumes only.
|
||||
|
||||
The package owns the artifact path. The loader checks the package version,
|
||||
ABI, Node-API compatibility, PE architecture, and SHA-256 before loading it.
|
||||
There is no environment override for loading an arbitrary native module.
|
||||
|
||||
## Build
|
||||
|
||||
The artifact lane requires Linux, MinGW-w64 `x86_64-w64-mingw32-g++`,
|
||||
`x86_64-w64-mingw32-dlltool`, and `tar`. It never installs these commands.
|
||||
Node-API headers are reused from the build cache or downloaded from the
|
||||
pinned official Node release with its published SHA-256 checked.
|
||||
|
||||
```sh
|
||||
npm run build:windows-native
|
||||
npm run verify:windows-native:artifact
|
||||
npm pack
|
||||
```
|
||||
|
||||
`build/` is ignored by Git. The npm file inventory includes only the addon and
|
||||
its manifest. Release CI builds both, and `prepublishOnly` rejects a missing,
|
||||
stale, mismatched, or wrong-architecture artifact. A development tarball built
|
||||
without this lane reports Windows native controls as unavailable.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Opaque Node objects carry a native type tag, resource kind, and closed state.
|
||||
A pathname, serialized object, or forged handle cannot grant authority.
|
||||
- Input admission opens every component with `OPEN_REPARSE_POINT`, verifies
|
||||
the actual NTFS object, and retains handles that deny replacement and source
|
||||
writing. An admitted child keeps each ancestor nonempty; ancestor handles
|
||||
permit unrelated writes while NTFS rejects in-place reparse conversion. Volume serial, 128-bit file ID, final handle path,
|
||||
requested path, size, and digest observations are preserved separately.
|
||||
- New runtime roots and private child objects are created relative to admitted
|
||||
parent handles with `NtCreateFile`. A protected DACL and current-user owner
|
||||
are installed at creation. Readback permits exactly the current user and SYSTEM. Child
|
||||
files inherit this boundary; private files are also checked by handle.
|
||||
- Snapshots stream through a 64 KiB buffer on a Node async worker. Cancellation
|
||||
is checked between reads, cleanup waits for settlement, and the digest
|
||||
commits to the bytes written. Descriptor and snapshot handles remain held
|
||||
until runtime cleanup.
|
||||
Snapshot ownership is single-flight per runtime: an overlapping request fails
|
||||
with `ERROR_BUSY` before acquiring cancellation ownership. The TypeScript owner
|
||||
records only the accepted promise and abort listener; aborting a rejected copy
|
||||
must not cancel the active copy.
|
||||
- `PROC_THREAD_ATTRIBUTE_JOB_LIST` assigns a suspended child atomically to an
|
||||
unnamed, non-inherited Job Object. Membership and kill-on-close policy are
|
||||
checked before resume. Breakaway is disabled, and cleanup waits for all
|
||||
job members and captured output, including detached descendants.
|
||||
- Only the suspended child's default file owner is normalized. Caller token
|
||||
privileges and ownership remain unchanged. Caller CPU affinity and process
|
||||
priority are preserved in the child. Environment names are sorted and
|
||||
deduplicated using Windows ordinal comparison, preserving their native Unicode
|
||||
semantics and caller-selected values.
|
||||
- Cleanup walks locked directory objects, removes files by handle, and unlinks
|
||||
reparse entries without traversing their targets. Normal close, cancellation,
|
||||
startup failure, and timeout release provider resources. Abrupt owner death
|
||||
kills the owned job; it can leave private runtime files for later operator
|
||||
cleanup and does not claim transactional deletion after a crash.
|
||||
|
||||
Windows failures retain their constraint, requested coordinate, Win32 code,
|
||||
and system message. Unsupported filesystems and reparse paths are distinct
|
||||
from OS access denial and missing packaged controls.
|
||||
|
||||
The bearer-token descriptor remains under its immutable private lease until
|
||||
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
|
||||
it. Job ownership guarantees process termination on owner death, not deletion
|
||||
of private files after a crash. SUBST drive aliases and changing DOS-device
|
||||
namespaces are outside the verified P0 scope and are not detected as a separate
|
||||
alias policy. Mounted-folder paths that report a reparse tag are rejected by
|
||||
component admission; broader namespace variants remain unverified. Preserve
|
||||
requested and final handle coordinates rather than treating them as one identity.
|
||||
|
||||
## Real verification
|
||||
|
||||
```powershell
|
||||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
```
|
||||
|
||||
The native lane checks DACL readback independently, source write/replacement
|
||||
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
|
||||
asynchronous cancellation, cleanup containment,
|
||||
descendant termination, unrelated-process preservation, normal exit, owner
|
||||
close, and forced owner exit. An optional independently built
|
||||
`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
|
||||
caller/child token observations and an in-place ancestor reparse attempt:
|
||||
|
||||
```powershell
|
||||
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
|
||||
```
|
||||
|
||||
`nonAdminRunner.cc` is a test-only launcher for an elevated development shell.
|
||||
It duplicates an existing non-elevated Explorer token belonging to the same
|
||||
user, creates private temporary window-station objects, and launches the test
|
||||
inside a kill-on-close fixture job. It does not create accounts, modify host
|
||||
policy, or change the caller's token. A real-provider claim must additionally
|
||||
verify the packaged CLI and MCP on that ordinary-user token.
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
static const napi_type_tag RESOURCE_TAG = {0x45fcbf0b49e84fe3ULL, 0x9e3e5074130de843ULL};
|
||||
|
||||
void check(napi_status status) {
|
||||
if (status != napi_ok) throw Failure("Invalid native boundary value", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
napi_value object(napi_env env) { napi_value value; check(napi_create_object(env, &value)); return value; }
|
||||
napi_value null(napi_env env) { napi_value value; check(napi_get_null(env, &value)); return value; }
|
||||
napi_value string(napi_env env, const std::string& text) {
|
||||
napi_value value; check(napi_create_string_utf8(env, text.data(), text.size(), &value)); return value;
|
||||
}
|
||||
napi_value string(napi_env env, const std::wstring& text) {
|
||||
static_assert(sizeof(wchar_t) == sizeof(char16_t));
|
||||
napi_value value;
|
||||
check(napi_create_string_utf16(env, reinterpret_cast<const char16_t*>(text.data()), text.size(), &value));
|
||||
return value;
|
||||
}
|
||||
napi_value number(napi_env env, double numeric) {
|
||||
napi_value value; check(napi_create_double(env, numeric, &value)); return value;
|
||||
}
|
||||
napi_value boolean(napi_env env, bool flag) {
|
||||
napi_value value; check(napi_get_boolean(env, flag, &value)); return value;
|
||||
}
|
||||
void set(napi_env env, napi_value target, const char* key, napi_value value) {
|
||||
check(napi_set_named_property(env, target, key, value));
|
||||
}
|
||||
std::wstring wide(napi_env env, napi_value value) {
|
||||
size_t size = 0;
|
||||
check(napi_get_value_string_utf16(env, value, nullptr, 0, &size));
|
||||
require(size <= 32767, "Windows value exceeds the native string limit", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<char16_t> buffer(size + 1);
|
||||
check(napi_get_value_string_utf16(env, value, buffer.data(), buffer.size(), &size));
|
||||
std::wstring result(reinterpret_cast<const wchar_t*>(buffer.data()), size);
|
||||
require(result.find(L'\0') == std::wstring::npos, "Windows value contains NUL", L"", ERROR_INVALID_PARAMETER);
|
||||
return result;
|
||||
}
|
||||
double numeric(napi_env env, napi_value value) {
|
||||
double result; check(napi_get_value_double(env, value, &result)); return result;
|
||||
}
|
||||
std::vector<napi_value> array(napi_env env, napi_value value) {
|
||||
bool valid; check(napi_is_array(env, value, &valid));
|
||||
require(valid, "Native arguments must be an array", L"", ERROR_INVALID_PARAMETER);
|
||||
uint32_t length; check(napi_get_array_length(env, value, &length));
|
||||
std::vector<napi_value> result(length);
|
||||
for (uint32_t index = 0; index < length; ++index)
|
||||
check(napi_get_element(env, value, index, &result[index]));
|
||||
return result;
|
||||
}
|
||||
static void finalize(napi_env, void* data, void*) { delete static_cast<Resource*>(data); }
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> value) {
|
||||
auto result = object(env);
|
||||
check(napi_type_tag_object(env, result, &RESOURCE_TAG));
|
||||
check(napi_wrap(env, result, value.get(), finalize, nullptr, nullptr));
|
||||
value.release();
|
||||
return result;
|
||||
}
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind) {
|
||||
bool tagged = false;
|
||||
check(napi_check_object_type_tag(env, value, &RESOURCE_TAG, &tagged));
|
||||
require(tagged, "Native handle does not belong to REA", L"", ERROR_INVALID_HANDLE);
|
||||
void* pointer = nullptr; check(napi_unwrap(env, value, &pointer));
|
||||
auto& result = *static_cast<Resource*>(pointer);
|
||||
require(result.kind == kind && !result.closed, "Native handle is closed or has the wrong kind", L"", ERROR_INVALID_HANDLE);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value failureValue(napi_env env, const Failure& failure) {
|
||||
wchar_t* message = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, failure.win32, 0, reinterpret_cast<wchar_t*>(&message), 0, nullptr);
|
||||
std::wstring detail = message == nullptr ? L"" : message;
|
||||
if (message != nullptr) LocalFree(message);
|
||||
const std::wstring constraint(failure.constraint.begin(), failure.constraint.end());
|
||||
auto text = string(env, constraint + (failure.path.empty() ? L"" : L" [" + failure.path + L"]") +
|
||||
L" (Win32 " + std::to_wstring(failure.win32) + L"): " + detail);
|
||||
napi_value error; napi_create_error(env, nullptr, text, &error);
|
||||
const char* code = failure.win32 == ERROR_FILE_NOT_FOUND || failure.win32 == ERROR_PATH_NOT_FOUND ? "ENOENT"
|
||||
: failure.win32 == ERROR_ACCESS_DENIED ? "EACCES" : "ERR_REA_WINDOWS_NATIVE";
|
||||
set(env, error, "code", string(env, std::string(code)));
|
||||
set(env, error, "constraint", string(env, failure.constraint));
|
||||
set(env, error, "requestedPath", string(env, failure.path));
|
||||
set(env, error, "win32Code", number(env, failure.win32));
|
||||
set(env, error, "win32Message", string(env, detail));
|
||||
return error;
|
||||
}
|
||||
|
||||
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));
|
||||
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);
|
||||
}
|
||||
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
|
||||
return filesystemCall(env, operation, values);
|
||||
} catch (const Failure& failure) {
|
||||
auto error = failureValue(env, failure);
|
||||
napi_throw(env, error);
|
||||
return nullptr;
|
||||
} catch (const std::exception& failure) {
|
||||
napi_throw_error(env, "ERR_REA_WINDOWS_NATIVE", failure.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static napi_value initialize(napi_env env, napi_value exports) {
|
||||
napi_value function;
|
||||
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
|
||||
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, rea::initialize)
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <aclapi.h>
|
||||
#include <bcrypt.h>
|
||||
#include <node_api.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rea {
|
||||
|
||||
struct Failure : std::runtime_error {
|
||||
DWORD win32;
|
||||
std::wstring path;
|
||||
std::string constraint;
|
||||
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
|
||||
: std::runtime_error(constraint), win32(code), path(std::move(path)),
|
||||
constraint(std::move(constraint)) {}
|
||||
};
|
||||
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path = L"") {
|
||||
if (!condition) {
|
||||
const auto code = GetLastError();
|
||||
throw Failure(constraint, path, code);
|
||||
}
|
||||
}
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path, DWORD code) {
|
||||
if (!condition) throw Failure(constraint, path, code);
|
||||
}
|
||||
|
||||
class Handle {
|
||||
HANDLE value_ = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
Handle() = default;
|
||||
explicit Handle(HANDLE value) : value_(value) {}
|
||||
~Handle() { reset(); }
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
Handle(Handle&& other) noexcept : value_(other.release()) {}
|
||||
Handle& operator=(Handle&& other) noexcept { reset(other.release()); return *this; }
|
||||
HANDLE get() const { return value_; }
|
||||
bool valid() const { return value_ != INVALID_HANDLE_VALUE && value_ != nullptr; }
|
||||
HANDLE release() { return std::exchange(value_, INVALID_HANDLE_VALUE); }
|
||||
void reset(HANDLE next = INVALID_HANDLE_VALUE) {
|
||||
if (valid()) CloseHandle(value_);
|
||||
value_ = next;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Kind { File, Runtime, Process };
|
||||
struct Resource {
|
||||
const Kind kind;
|
||||
bool closed = false;
|
||||
explicit Resource(Kind value) : kind(value) {}
|
||||
virtual ~Resource() = default;
|
||||
};
|
||||
struct File : Resource {
|
||||
std::wstring requestedPath;
|
||||
std::wstring path;
|
||||
std::vector<Handle> handles;
|
||||
explicit File(std::wstring path) : Resource(Kind::File), requestedPath(path), path(std::move(path)) {}
|
||||
HANDLE get() const { return handles.back().get(); }
|
||||
};
|
||||
struct Runtime : Resource {
|
||||
std::wstring path;
|
||||
std::vector<Handle> parents;
|
||||
Handle directory;
|
||||
std::vector<Handle> immutableFiles;
|
||||
// Main-thread admission is single-flight; only the accepted copy owns the
|
||||
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
|
||||
// another copy before it can reset that flag or replace pending ownership.
|
||||
bool snapshotPending = false;
|
||||
std::atomic_bool snapshotCancelled{false};
|
||||
Runtime() : Resource(Kind::Runtime) {}
|
||||
};
|
||||
struct Process : Resource {
|
||||
Handle job, process, stdoutRead, stderrRead;
|
||||
DWORD pid = 0;
|
||||
Process() : Resource(Kind::Process) {}
|
||||
};
|
||||
|
||||
void check(napi_status status);
|
||||
napi_value failureValue(napi_env env, const Failure& failure);
|
||||
napi_value object(napi_env env);
|
||||
napi_value string(napi_env env, const std::string& value);
|
||||
napi_value string(napi_env env, const std::wstring& value);
|
||||
napi_value number(napi_env env, double value);
|
||||
napi_value boolean(napi_env env, bool value);
|
||||
napi_value null(napi_env env);
|
||||
void set(napi_env env, napi_value object, const char* key, napi_value value);
|
||||
std::wstring wide(napi_env env, napi_value value);
|
||||
double numeric(napi_env env, napi_value value);
|
||||
std::vector<napi_value> array(napi_env env, napi_value value);
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind);
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
|
||||
bool directory = false);
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
|
||||
void closeRuntime(Runtime& runtime);
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment);
|
||||
napi_value processCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
napi_value inspect(napi_env env);
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,531 @@
|
||||
#include "authority.hpp"
|
||||
#include <sddl.h>
|
||||
#include <winternl.h>
|
||||
|
||||
namespace rea {
|
||||
|
||||
static std::wstring hex(const unsigned char* bytes, size_t count) {
|
||||
constexpr wchar_t digits[] = L"0123456789abcdef";
|
||||
std::wstring result;
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring localPath(std::wstring path) {
|
||||
if (path.rfind(L"\\\\?\\", 0) == 0) path.erase(0, 4);
|
||||
require(path.size() >= 3 && path[1] == L':' && path[2] == L'\\' &&
|
||||
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')),
|
||||
"Only absolute local NTFS drive paths are supported", path, ERROR_NOT_SUPPORTED);
|
||||
require(path.find(L'/', 0) == std::wstring::npos && path.find(L':', 2) == std::wstring::npos,
|
||||
"Alternate streams and non-local path syntax are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
require(GetDriveTypeW(path.substr(0, 3).c_str()) == DRIVE_FIXED,
|
||||
"Only fixed local NTFS volumes are supported", path, ERROR_NOT_SUPPORTED);
|
||||
if (path.size() > 3 && path.back() == L'\\') path.pop_back();
|
||||
require(path.size() == 3 || path.back() != L'\\', "Repeated path separators are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
size_t begin = 3;
|
||||
while (begin < path.size()) {
|
||||
const auto end = path.find(L'\\', begin);
|
||||
const auto component = path.substr(begin, end == std::wstring::npos ? end : end - begin);
|
||||
require(!component.empty() && component != L"." && component != L".." &&
|
||||
component.back() != L'.' && component.back() != L' ',
|
||||
"Ambiguous Windows path component is unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
if (end == std::wstring::npos) break;
|
||||
begin = end + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static Handle openComponent(const std::wstring& path, DWORD access, bool directory, DWORD sharing = FILE_SHARE_READ) {
|
||||
// Attribute-only opens do not participate in Windows share-access checks.
|
||||
// Directory guards need FILE_LIST_DIRECTORY to prevent replacement and
|
||||
// in-place reparse conversion while descendant paths are being consumed.
|
||||
Handle handle(CreateFileW((L"\\\\?\\" + path).c_str(), access | FILE_READ_ATTRIBUTES | READ_CONTROL |
|
||||
(directory ? FILE_LIST_DIRECTORY : 0),
|
||||
sharing, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(handle.valid(), "Open Windows path component failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle.get(), FileAttributeTagInfo, &info, sizeof(info)),
|
||||
"Read Windows path attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
||||
throw Failure("Reparse path component is unsupported (tag " + std::to_string(info.ReparseTag) + ")",
|
||||
path, ERROR_NOT_SUPPORTED);
|
||||
require(((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory,
|
||||
directory ? "Expected a directory path component" : "Target is not a regular file", path,
|
||||
directory ? ERROR_PATH_NOT_FOUND : ERROR_DIRECTORY);
|
||||
require(GetFileType(handle.get()) == FILE_TYPE_DISK, "Windows object is not a disk file", path, ERROR_NOT_SUPPORTED);
|
||||
return handle;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
|
||||
auto result = std::make_unique<File>(localPath(requested));
|
||||
result->requestedPath = requested;
|
||||
const auto& path = result->path;
|
||||
require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY);
|
||||
result->handles.push_back(openComponent(path.substr(0, 3), 0, true));
|
||||
wchar_t filesystem[64]{};
|
||||
DWORD flags = 0;
|
||||
require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr,
|
||||
&flags, filesystem, 64), "Read volume semantics failed", path);
|
||||
require(std::wstring(filesystem) == L"NTFS" && (flags & FILE_PERSISTENT_ACLS) != 0,
|
||||
"Backing volume must be NTFS with persistent ACLs", path, ERROR_NOT_SUPPORTED);
|
||||
size_t offset = 3;
|
||||
while (offset < path.size()) {
|
||||
auto end = path.find(L'\\', offset);
|
||||
const bool final = end == std::wstring::npos;
|
||||
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
|
||||
// Once the next component is pinned against deletion, its parent cannot
|
||||
// become empty and NTFS cannot convert that parent into a reparse point.
|
||||
// Relax only ancestor write sharing so unrelated child renames can proceed.
|
||||
// Independent evidence: tests/fixtures/windows/processBoundary.cc attempts
|
||||
// FSCTL_SET_REPARSE_POINT on the nonempty pinned ancestor and observes denial.
|
||||
// SUBST aliases and mounted-folder/reparse namespace variants are outside
|
||||
// the admitted P0 evidence; requested and final handle paths remain distinct.
|
||||
const auto parentPath = offset == 3 ? path.substr(0, 3) : path.substr(0, offset - 1);
|
||||
result->handles.back() = openComponent(parentPath, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->handles.push_back(std::move(child));
|
||||
if (final) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path) {
|
||||
FILE_ID_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileIdInfo, &info, sizeof(info)),
|
||||
"Stable 128-bit Windows file identity is unavailable", path);
|
||||
std::array<wchar_t, 32768> finalPath;
|
||||
const auto length = GetFinalPathNameByHandleW(handle, finalPath.data(), finalPath.size(), FILE_NAME_NORMALIZED);
|
||||
require(length > 0 && length < finalPath.size(), "Read final handle path failed", path);
|
||||
LARGE_INTEGER size{};
|
||||
FILE_STANDARD_INFO standard{};
|
||||
require(GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard)),
|
||||
"Read handle metadata failed", path);
|
||||
size = standard.EndOfFile;
|
||||
require(size.QuadPart >= 0 && size.QuadPart <= 9007199254740991LL, "File size exceeds exact numeric representation", path,
|
||||
ERROR_NOT_SUPPORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "requestedPath", string(env, path));
|
||||
set(env, result, "finalPath", string(env, std::wstring(finalPath.data(), length)));
|
||||
set(env, result, "filesystem", string(env, std::string("NTFS")));
|
||||
std::array<unsigned char, 8> serial;
|
||||
for (size_t index = 0; index < serial.size(); ++index)
|
||||
serial[index] = static_cast<unsigned char>(info.VolumeSerialNumber >> ((7 - index) * 8));
|
||||
set(env, result, "volumeSerial", string(env, hex(serial.data(), serial.size())));
|
||||
set(env, result, "fileId", string(env, hex(info.FileId.Identifier, 16)));
|
||||
set(env, result, "size", number(env, static_cast<double>(size.QuadPart)));
|
||||
set(env, result, "directory", boolean(env, standard.Directory != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Security {
|
||||
std::vector<unsigned char> user;
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> system{};
|
||||
std::vector<unsigned char> acl;
|
||||
SECURITY_DESCRIPTOR descriptor{};
|
||||
PSID userSid() const { return reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid; }
|
||||
Security() {
|
||||
Handle token;
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw), "Read current user token failed");
|
||||
token.reset(raw);
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &size);
|
||||
user.resize(size);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), size, &size), "Read current user SID failed");
|
||||
DWORD systemSize = system.size();
|
||||
require(CreateWellKnownSid(WinLocalSystemSid, nullptr, system.data(), &systemSize), "Create SYSTEM SID failed");
|
||||
acl.resize(sizeof(ACL) + 2 * (sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD)) + GetLengthSid(userSid()) + systemSize);
|
||||
auto access = reinterpret_cast<PACL>(acl.data());
|
||||
require(InitializeAcl(access, acl.size(), ACL_REVISION), "Initialize private DACL failed");
|
||||
for (PSID sid : {userSid(), static_cast<PSID>(system.data())})
|
||||
require(AddAccessAllowedAceEx(access, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
|
||||
FILE_ALL_ACCESS, sid), "Populate private DACL failed");
|
||||
require(InitializeSecurityDescriptor(&descriptor, SECURITY_DESCRIPTOR_REVISION) &&
|
||||
SetSecurityDescriptorOwner(&descriptor, userSid(), FALSE) &&
|
||||
SetSecurityDescriptorDacl(&descriptor, TRUE, access, FALSE) &&
|
||||
SetSecurityDescriptorControl(&descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED),
|
||||
"Create protected security descriptor failed");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD access, bool directory,
|
||||
Security& security, const std::wstring& path) {
|
||||
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
|
||||
"Private object requires one filename component", path, ERROR_INVALID_PARAMETER);
|
||||
UNICODE_STRING coordinate{};
|
||||
coordinate.Buffer = const_cast<wchar_t*>(name.data());
|
||||
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
|
||||
coordinate.MaximumLength = coordinate.Length;
|
||||
OBJECT_ATTRIBUTES attributes;
|
||||
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, &security.descriptor);
|
||||
IO_STATUS_BLOCK observation{};
|
||||
HANDLE raw = INVALID_HANDLE_VALUE;
|
||||
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
|
||||
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
|
||||
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
|
||||
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
|
||||
RtlNtStatusToDosError(status));
|
||||
Handle result(raw);
|
||||
require(result.valid() && observation.Information == FILE_CREATED,
|
||||
"Private creation did not return a new object handle", path, ERROR_ACCESS_DENIED);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
|
||||
Security expected;
|
||||
PSID owner = nullptr;
|
||||
PACL acl = nullptr;
|
||||
PSECURITY_DESCRIPTOR raw = nullptr;
|
||||
const DWORD code = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&owner, nullptr, &acl, nullptr, &raw);
|
||||
if (code != ERROR_SUCCESS) throw Failure("Read private DACL by handle failed", path, code);
|
||||
const auto descriptor = std::unique_ptr<void, decltype(&LocalFree)>(raw, LocalFree);
|
||||
require(owner != nullptr && EqualSid(owner, expected.userSid()), "Private object owner is not the current user", path,
|
||||
ERROR_ACCESS_DENIED);
|
||||
SECURITY_DESCRIPTOR_CONTROL control;
|
||||
DWORD revision;
|
||||
require(GetSecurityDescriptorControl(raw, &control, &revision), "Read DACL control flags failed", path);
|
||||
require(acl != nullptr && (control & SE_DACL_PRESENT) != 0 && (!root || (control & SE_DACL_PROTECTED) != 0),
|
||||
"Private object requires a present protected DACL", path, ERROR_ACCESS_DENIED);
|
||||
require(acl->AceCount == 2, "Private DACL contains unexpected trustees", path, ERROR_ACCESS_DENIED);
|
||||
bool user = false, system = false;
|
||||
for (DWORD index = 0; index < acl->AceCount; ++index) {
|
||||
void* rawAce;
|
||||
require(GetAce(acl, index, &rawAce), "Read private DACL entry failed", path);
|
||||
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(rawAce);
|
||||
require(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && ace->Mask == FILE_ALL_ACCESS &&
|
||||
(ace->Header.AceFlags & INHERIT_ONLY_ACE) == 0 &&
|
||||
(!root || (ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
|
||||
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)),
|
||||
"Private DACL contains unsupported rights or ACE type", path, ERROR_ACCESS_DENIED);
|
||||
PSID sid = const_cast<DWORD*>(&ace->SidStart);
|
||||
if (EqualSid(sid, expected.userSid())) user = true;
|
||||
else if (EqualSid(sid, expected.system.data())) system = true;
|
||||
else throw Failure("Private DACL permits an unrelated principal", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
|
||||
auto parent = openFile(parentPath, 0, true);
|
||||
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
|
||||
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
|
||||
std::array<unsigned char, 16> random;
|
||||
require(BCryptGenRandom(nullptr, random.data(), random.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0,
|
||||
"Generate runtime identity failed", parentPath, ERROR_GEN_FAILURE);
|
||||
auto result = std::make_unique<Runtime>();
|
||||
result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());
|
||||
Security security;
|
||||
result->directory = createRelative(parent->get(), prefix + hex(random.data(), random.size()),
|
||||
DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, result->path);
|
||||
try {
|
||||
verifyPrivate(result->directory.get(), result->path, true);
|
||||
// The owned child pins the parent nonempty before write sharing is relaxed.
|
||||
parent->handles.back() = openComponent(parent->path, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->parents = std::move(parent->handles);
|
||||
} catch (...) {
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
SetFileInformationByHandle(result->directory.get(), FileDispositionInfo, &deletion, sizeof(deletion));
|
||||
throw;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring relativePath(Runtime& root, const std::wstring& relative) {
|
||||
require(!relative.empty() && relative[0] != L'\\' && relative.find(L':') == std::wstring::npos,
|
||||
"Runtime coordinate must be a relative path", relative, ERROR_INVALID_PARAMETER);
|
||||
return localPath(root.path + L"\\" + relative);
|
||||
}
|
||||
|
||||
static std::unique_ptr<File> runtimeDirectory(Runtime& root, const std::wstring& path) {
|
||||
auto result = std::make_unique<File>(path);
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate private directory handle failed", path);
|
||||
result->handles.emplace_back(duplicate);
|
||||
size_t offset = root.path.size() + 1;
|
||||
while (offset < path.size()) {
|
||||
const auto end = path.find(L'\\', offset);
|
||||
result->handles.push_back(openComponent(path.substr(0, end), 0, true));
|
||||
if (end == std::wstring::npos) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void directory(Runtime& root, const std::wstring& relative) {
|
||||
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;
|
||||
try {
|
||||
auto created = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
|
||||
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, path);
|
||||
verifyPrivate(created.get(), path, false);
|
||||
} catch (const Failure& failure) {
|
||||
if (failure.win32 != ERROR_ALREADY_EXISTS && failure.win32 != ERROR_FILE_EXISTS) throw;
|
||||
auto existing = runtimeDirectory(root, path);
|
||||
verifyPrivate(existing->get(), path, false);
|
||||
}
|
||||
}
|
||||
|
||||
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
|
||||
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),
|
||||
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
|
||||
verifyPrivate(file.get(), path, false);
|
||||
return file;
|
||||
}
|
||||
|
||||
static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::wstring& path) {
|
||||
DWORD written;
|
||||
require(WriteFile(file, bytes, count, &written, nullptr) && written == count, "Write private file failed", path);
|
||||
}
|
||||
|
||||
static void dispose(HANDLE handle, const std::wstring& path) {
|
||||
FILE_BASIC_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
|
||||
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
|
||||
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
|
||||
}
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
|
||||
}
|
||||
|
||||
void closeRuntime(Runtime& root) {
|
||||
if (root.closed) return;
|
||||
// Windows keeps the bearer descriptor and snapshot leases until this owner
|
||||
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
|
||||
// closes process jobs but cannot run this walk, so private files can remain.
|
||||
root.immutableFiles.clear();
|
||||
struct ReleaseRuntime {
|
||||
Runtime& root;
|
||||
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
|
||||
} release{root};
|
||||
// Each traversed directory stays locked against replacement. Reparse entries
|
||||
// are unlinked as objects; their targets are never enumerated or deleted.
|
||||
struct Frame {
|
||||
Handle directory;
|
||||
HANDLE search = INVALID_HANDLE_VALUE;
|
||||
std::wstring path;
|
||||
WIN32_FIND_DATAW next{};
|
||||
bool pending = false;
|
||||
Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
|
||||
search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
|
||||
FindExSearchNameMatch, nullptr, 0);
|
||||
pending = search != INVALID_HANDLE_VALUE;
|
||||
require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
|
||||
}
|
||||
~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
|
||||
};
|
||||
std::vector<std::unique_ptr<Frame>> frames;
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate runtime cleanup handle failed", root.path);
|
||||
frames.push_back(std::make_unique<Frame>(Handle(duplicate), root.path));
|
||||
while (!frames.empty()) {
|
||||
auto& frame = *frames.back();
|
||||
if (!frame.pending) {
|
||||
if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
|
||||
dispose(frame.directory.get(), frame.path);
|
||||
frames.pop_back();
|
||||
continue;
|
||||
}
|
||||
const auto name = std::wstring(frame.next.cFileName);
|
||||
frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
|
||||
require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
|
||||
if (name == L"." || name == L"..") continue;
|
||||
const auto path = frame.path + L"\\" + name;
|
||||
Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
|
||||
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(child.valid(), "Open owned cleanup object failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
|
||||
frames.push_back(std::make_unique<Frame>(std::move(child), path));
|
||||
else dispose(child.get(), path);
|
||||
}
|
||||
}
|
||||
|
||||
struct SnapshotWork {
|
||||
napi_env env;
|
||||
napi_async_work work = nullptr;
|
||||
napi_deferred deferred = nullptr;
|
||||
napi_ref rootReference = nullptr;
|
||||
Runtime* root;
|
||||
std::unique_ptr<File> source;
|
||||
Handle destination;
|
||||
std::wstring path;
|
||||
std::wstring digest;
|
||||
std::unique_ptr<Failure> failure;
|
||||
SnapshotWork(napi_env env, Runtime& root) : env(env), root(&root) {}
|
||||
};
|
||||
|
||||
static void copySnapshot(napi_env, void* pointer) {
|
||||
auto& work = *static_cast<SnapshotWork*>(pointer);
|
||||
try {
|
||||
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
||||
require(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) == 0,
|
||||
"Initialize snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Algorithm { BCRYPT_ALG_HANDLE handle; ~Algorithm() { BCryptCloseAlgorithmProvider(handle, 0); } } owner{algorithm};
|
||||
BCRYPT_HASH_HANDLE hash = nullptr;
|
||||
require(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) == 0,
|
||||
"Create snapshot SHA-256 state failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Hash { BCRYPT_HASH_HANDLE handle; ~Hash() { BCryptDestroyHash(handle); } } hashOwner{hash};
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
while (true) {
|
||||
require(!work.root->snapshotCancelled.load(), "Admitted snapshot was cancelled", work.path, ERROR_OPERATION_ABORTED);
|
||||
DWORD count;
|
||||
require(ReadFile(work.source->get(), buffer.data(), buffer.size(), &count, nullptr),
|
||||
"Read admitted source handle failed", work.source->path);
|
||||
if (count == 0) break;
|
||||
writeBytes(work.destination.get(), buffer.data(), count, work.path);
|
||||
require(BCryptHashData(hash, buffer.data(), count, 0) == 0,
|
||||
"Hash admitted snapshot bytes failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
std::array<unsigned char, 32> digest;
|
||||
require(BCryptFinishHash(hash, digest.data(), digest.size(), 0) == 0,
|
||||
"Finish snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
require(FlushFileBuffers(work.destination.get()), "Flush private snapshot failed", work.path);
|
||||
work.digest = hex(digest.data(), digest.size());
|
||||
} catch (const Failure& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure);
|
||||
} catch (const std::exception& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure.what(), work.path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
|
||||
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
|
||||
work->root->snapshotPending = false;
|
||||
try {
|
||||
if (work->failure) throw *work->failure;
|
||||
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "sha256", string(env, work->digest));
|
||||
set(env, result, "source", identity(env, work->source->get(), work->source->requestedPath));
|
||||
set(env, result, "snapshot", identity(env, work->destination.get(), work->path));
|
||||
work->root->immutableFiles.push_back(std::move(work->destination));
|
||||
check(napi_resolve_deferred(env, work->deferred, result));
|
||||
} catch (const Failure& failure) {
|
||||
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
|
||||
} catch (const std::exception& failure) {
|
||||
napi_reject_deferred(env, work->deferred,
|
||||
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
|
||||
}
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
napi_delete_async_work(env, work->work);
|
||||
}
|
||||
|
||||
static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
|
||||
const std::wstring& sourcePath, const std::wstring& relative) {
|
||||
auto work = std::make_unique<SnapshotWork>(env, root);
|
||||
work->source = openFile(sourcePath);
|
||||
work->path = relativePath(root, relative);
|
||||
work->destination = createPrivateFile(root, relative);
|
||||
napi_value promise;
|
||||
check(napi_create_promise(env, &work->deferred, &promise));
|
||||
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
|
||||
const auto label = string(env, std::string("REA admitted Windows snapshot"));
|
||||
root.snapshotCancelled.store(false);
|
||||
try {
|
||||
check(napi_create_async_work(env, nullptr, label, copySnapshot, completeSnapshot, work.get(), &work->work));
|
||||
check(napi_queue_async_work(env, work->work));
|
||||
} catch (...) {
|
||||
if (work->work) napi_delete_async_work(env, work->work);
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
throw;
|
||||
}
|
||||
root.snapshotPending = true;
|
||||
work.release();
|
||||
return promise;
|
||||
}
|
||||
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
|
||||
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
|
||||
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
if (operation == L"open") {
|
||||
auto file = openFile(wide(env, args[0]));
|
||||
auto result = identity(env, file->get(), file->requestedPath);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"close") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
file.handles.clear(); file.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"read") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
const auto position = numeric(env, args[1]);
|
||||
const auto length = numeric(env, args[2]);
|
||||
require(position >= 0 && position <= 9007199254740991.0 && position == static_cast<int64_t>(position) &&
|
||||
length >= 0 && length <= 65536 && length == static_cast<DWORD>(length),
|
||||
"Read coordinates exceed the native chunk boundary", file.path, ERROR_INVALID_PARAMETER);
|
||||
LARGE_INTEGER offset; offset.QuadPart = static_cast<int64_t>(position);
|
||||
require(SetFilePointerEx(file.get(), offset, nullptr, FILE_BEGIN), "Seek admitted file handle failed", file.path);
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
DWORD count;
|
||||
require(ReadFile(file.get(), buffer.data(), static_cast<DWORD>(length), &count, nullptr), "Read admitted file handle failed", file.path);
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, buffer.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_create") {
|
||||
auto root = createRuntime(wide(env, args[0]), wide(env, args[1]));
|
||||
auto result = identity(env, root->directory.get(), root->path);
|
||||
set(env, result, "path", string(env, root->path));
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(root)));
|
||||
return result;
|
||||
}
|
||||
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
|
||||
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
|
||||
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
|
||||
// Reject overlapping operations before snapshot() resets cancellation. The
|
||||
// one accepted worker and WindowsPrivateRuntime retain the same root lease.
|
||||
if (operation == L"runtime_close") { closeRuntime(root); return null(env); }
|
||||
if (operation == L"runtime_mkdir") { directory(root, wide(env, args[1])); return null(env); }
|
||||
if (operation == L"runtime_open") {
|
||||
const auto path = relativePath(root, wide(env, args[1]));
|
||||
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
|
||||
file->path = path;
|
||||
file->requestedPath = path;
|
||||
file->handles.push_back(openComponent(path, GENERIC_READ, false));
|
||||
verifyPrivate(file->get(), path, false);
|
||||
auto result = identity(env, file->get(), path);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_write") {
|
||||
bool isBuffer; check(napi_is_buffer(env, args[2], &isBuffer));
|
||||
require(isBuffer, "Private write requires a byte buffer", root.path, ERROR_INVALID_PARAMETER);
|
||||
void* bytes; size_t length; check(napi_get_buffer_info(env, args[2], &bytes, &length));
|
||||
require(length <= MAXDWORD, "Private write exceeds Win32 byte representation", root.path, ERROR_INVALID_PARAMETER);
|
||||
auto file = createPrivateFile(root, wide(env, args[1]));
|
||||
writeBytes(file.get(), bytes, static_cast<DWORD>(length), root.path);
|
||||
require(FlushFileBuffers(file.get()), "Flush private runtime file failed", root.path);
|
||||
root.immutableFiles.push_back(std::move(file));
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"runtime_snapshot") {
|
||||
return snapshot(env, args[0], root, wide(env, args[1]), wide(env, args[2]));
|
||||
}
|
||||
throw Failure("Unknown native filesystem operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,271 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
class Attributes {
|
||||
std::vector<unsigned char> storage_;
|
||||
public:
|
||||
explicit Attributes(DWORD count) {
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
|
||||
storage_.resize(size);
|
||||
require(InitializeProcThreadAttributeList(get(), count, 0, &size), "Initialize process attributes failed");
|
||||
}
|
||||
~Attributes() { DeleteProcThreadAttributeList(get()); }
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST get() { return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage_.data()); }
|
||||
void add(DWORD_PTR key, void* value, SIZE_T size) {
|
||||
require(UpdateProcThreadAttribute(get(), 0, key, value, size, nullptr, nullptr),
|
||||
"Windows atomic process attribute is unavailable");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createJob() {
|
||||
Handle job(CreateJobObjectW(nullptr, nullptr));
|
||||
require(job.valid(), "Create owned Job Object failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION policy{};
|
||||
policy.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
require(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &policy, sizeof(policy)),
|
||||
"Apply kill-on-owner-close Job Object policy failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION actual{};
|
||||
require(QueryInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &actual, sizeof(actual), nullptr),
|
||||
"Read Job Object policy failed");
|
||||
require(actual.BasicLimitInformation.LimitFlags == JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
"Job Object policy permits unexpected breakaway", L"", ERROR_ACCESS_DENIED);
|
||||
return job;
|
||||
}
|
||||
|
||||
static void pipe(Handle& read, Handle& write) {
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE rawRead, rawWrite;
|
||||
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
|
||||
read.reset(rawRead); write.reset(rawWrite);
|
||||
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
|
||||
}
|
||||
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
|
||||
require(command.size() >= 3 && command[1] == L':' && command[2] == L'\\',
|
||||
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
|
||||
auto result = std::make_unique<Process>();
|
||||
result->job = createJob();
|
||||
Handle stdoutWrite, stderrWrite;
|
||||
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
require(input.valid(), "Open owned process null input failed");
|
||||
Attributes attributes(2);
|
||||
HANDLE job = result->job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
|
||||
std::array<HANDLE, 3> inherited{input.get(), stdoutWrite.get(), stderrWrite.get()};
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited));
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = input.get();
|
||||
startup.StartupInfo.hStdOutput = stdoutWrite.get();
|
||||
startup.StartupInfo.hStdError = stderrWrite.get();
|
||||
startup.lpAttributeList = attributes.get();
|
||||
// Windows environment names use ordinal case-insensitive comparison, not
|
||||
// locale collation or JavaScript's expanding Unicode uppercase conversion.
|
||||
auto orderedEnvironment = environment;
|
||||
auto keyLength = [&](const std::wstring& entry) {
|
||||
const auto end = entry.find(L'=', entry.front() == L'=' ? 1 : 0);
|
||||
require(end != std::wstring::npos && end > 0, "Invalid child environment name", command,
|
||||
ERROR_INVALID_PARAMETER);
|
||||
return static_cast<int>(end);
|
||||
};
|
||||
for (const auto& entry : orderedEnvironment) {
|
||||
require(!entry.empty(), "Empty child environment entry", command, ERROR_INVALID_PARAMETER);
|
||||
keyLength(entry);
|
||||
}
|
||||
auto compareNames = [&](const std::wstring& left, const std::wstring& right) {
|
||||
const auto result = CompareStringOrdinal(left.data(), keyLength(left), right.data(), keyLength(right), TRUE);
|
||||
require(result != 0, "Compare Windows environment names failed", command);
|
||||
return result;
|
||||
};
|
||||
std::stable_sort(orderedEnvironment.begin(), orderedEnvironment.end(),
|
||||
[&](const std::wstring& left, const std::wstring& right) {
|
||||
return compareNames(left, right) == CSTR_LESS_THAN;
|
||||
});
|
||||
std::vector<std::wstring> uniqueEnvironment;
|
||||
for (auto& entry : orderedEnvironment) {
|
||||
if (!uniqueEnvironment.empty() && compareNames(uniqueEnvironment.back(), entry) == CSTR_EQUAL)
|
||||
uniqueEnvironment.back() = std::move(entry);
|
||||
else uniqueEnvironment.push_back(std::move(entry));
|
||||
}
|
||||
std::vector<wchar_t> variables;
|
||||
for (const auto& entry : uniqueEnvironment) {
|
||||
variables.insert(variables.end(), entry.begin(), entry.end()); variables.push_back(L'\0');
|
||||
}
|
||||
variables.push_back(L'\0');
|
||||
if (uniqueEnvironment.empty()) variables.push_back(L'\0');
|
||||
std::vector<wchar_t> line(commandLine.begin(), commandLine.end()); line.push_back(L'\0');
|
||||
PROCESS_INFORMATION process{};
|
||||
require(CreateProcessW(command.c_str(), line.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
|
||||
variables.data(), cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &process),
|
||||
"Create process atomically inside owned Job Object failed", command);
|
||||
result->process.reset(process.hProcess);
|
||||
Handle thread(process.hThread);
|
||||
result->pid = process.dwProcessId;
|
||||
// An elevated token may default file ownership to Administrators. Normalize
|
||||
// only the suspended child's default owner so its runtime files belong to the
|
||||
// current user; the caller's token and privileges are not modified.
|
||||
HANDLE rawToken;
|
||||
require(OpenProcessToken(result->process.get(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &rawToken),
|
||||
"Open suspended process default-owner token failed", command);
|
||||
Handle token(rawToken);
|
||||
DWORD tokenSize = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &tokenSize);
|
||||
std::vector<unsigned char> tokenUser(tokenSize);
|
||||
require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize),
|
||||
"Read suspended process user identity failed", command);
|
||||
TOKEN_OWNER owner{reinterpret_cast<TOKEN_USER*>(tokenUser.data())->User.Sid};
|
||||
require(SetTokenInformation(token.get(), TokenOwner, &owner, sizeof(owner)),
|
||||
"Set suspended process file owner failed", command);
|
||||
DWORD_PTR affinity = 0, systemAffinity = 0;
|
||||
require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
|
||||
"Read caller process affinity failed", command);
|
||||
if (affinity != 0)
|
||||
require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
|
||||
const DWORD priority = GetPriorityClass(GetCurrentProcess());
|
||||
require(priority != 0 && SetPriorityClass(result->process.get(), priority),
|
||||
"Preserve caller process priority failed", command);
|
||||
BOOL assigned = FALSE;
|
||||
if (!IsProcessInJob(result->process.get(), result->job.get(), &assigned) || !assigned) {
|
||||
TerminateJobObject(result->job.get(), 1);
|
||||
throw Failure("Suspended process has no owned Job Object membership", command, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(ResumeThread(thread.get()) != MAXDWORD, "Resume job-owned process failed", command);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
|
||||
std::array<unsigned char, 65536> bytes;
|
||||
DWORD available = 0, count = 0;
|
||||
if (!read.valid()) ended = true;
|
||||
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
|
||||
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
|
||||
read.reset(); ended = true;
|
||||
} else if (available > 0) {
|
||||
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
|
||||
"Read owned process output failed");
|
||||
}
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
static DWORD activeProcesses(Process& process) {
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION state{};
|
||||
require(QueryInformationJobObject(process.job.get(), JobObjectBasicAccountingInformation, &state, sizeof(state), nullptr),
|
||||
"Observe owned Job Object settlement failed");
|
||||
return state.ActiveProcesses;
|
||||
}
|
||||
|
||||
static napi_value callerToken(napi_env env) {
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &raw), "Read caller token failed");
|
||||
Handle token(raw);
|
||||
DWORD size;
|
||||
TOKEN_ELEVATION elevation{};
|
||||
require(GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size), "Read caller elevation failed");
|
||||
std::vector<unsigned char> user(4096), owner(4096), integrity(4096);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), user.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenOwner, owner.data(), owner.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenIntegrityLevel, integrity.data(), integrity.size(), &size),
|
||||
"Read caller token observations failed");
|
||||
HANDLE rawImpersonation;
|
||||
require(DuplicateToken(token.get(), SecurityIdentification, &rawImpersonation), "Observe caller group membership failed");
|
||||
Handle impersonation(rawImpersonation);
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> administrators;
|
||||
DWORD administratorSize = administrators.size();
|
||||
require(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, administrators.data(), &administratorSize),
|
||||
"Create administrator group identity failed");
|
||||
BOOL administrator = FALSE, assigned = FALSE;
|
||||
require(CheckTokenMembership(impersonation.get(), administrators.data(), &administrator), "Read caller administrator membership failed");
|
||||
require(IsProcessInJob(GetCurrentProcess(), nullptr, &assigned), "Read caller job membership failed");
|
||||
const auto sid = reinterpret_cast<TOKEN_MANDATORY_LABEL*>(integrity.data())->Label.Sid;
|
||||
auto result = object(env);
|
||||
set(env, result, "elevated", boolean(env, elevation.TokenIsElevated != 0));
|
||||
set(env, result, "administrator", boolean(env, administrator != FALSE));
|
||||
set(env, result, "integrityLevel", number(env, *GetSidSubAuthority(sid, *GetSidSubAuthorityCount(sid) - 1)));
|
||||
set(env, result, "defaultOwnerIsUser", boolean(env, EqualSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
|
||||
reinterpret_cast<TOKEN_OWNER*>(owner.data())->Owner)));
|
||||
set(env, result, "inJob", boolean(env, assigned != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
if (operation == L"process_caller_token") {
|
||||
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return callerToken(env);
|
||||
}
|
||||
if (operation == L"process_spawn") {
|
||||
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<std::wstring> environment;
|
||||
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
|
||||
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
|
||||
auto result = object(env);
|
||||
set(env, result, "pid", number(env, process->pid));
|
||||
set(env, result, "jobAssigned", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(process)));
|
||||
return result;
|
||||
}
|
||||
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
|
||||
if (operation == L"process_poll") {
|
||||
auto result = object(env);
|
||||
bool stdoutEnded = false, stderrEnded = false;
|
||||
set(env, result, "stdout", readOutput(env, process.stdoutRead, stdoutEnded));
|
||||
set(env, result, "stderr", readOutput(env, process.stderrRead, stderrEnded));
|
||||
set(env, result, "stdoutEnded", boolean(env, stdoutEnded));
|
||||
set(env, result, "stderrEnded", boolean(env, stderrEnded));
|
||||
const auto exited = WaitForSingleObject(process.process.get(), 0);
|
||||
require(exited == WAIT_OBJECT_0 || exited == WAIT_TIMEOUT, "Observe owned process exit failed");
|
||||
DWORD code = 0;
|
||||
if (exited == WAIT_OBJECT_0) require(GetExitCodeProcess(process.process.get(), &code), "Read owned process exit code failed");
|
||||
set(env, result, "exitCode", exited == WAIT_OBJECT_0 ? number(env, code) : null(env));
|
||||
set(env, result, "activeProcesses", number(env, activeProcesses(process)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"process_alive") return number(env, activeProcesses(process));
|
||||
if (operation == L"process_terminate") {
|
||||
const auto active = activeProcesses(process);
|
||||
if (active > 0) require(TerminateJobObject(process.job.get(), 1), "Terminate owned Job Object failed");
|
||||
return boolean(env, active > 0);
|
||||
}
|
||||
if (operation == L"process_close") {
|
||||
// Closing the job is the crash-safe authority; no PID enumeration is used.
|
||||
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
|
||||
process.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
napi_value inspect(napi_env env) {
|
||||
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);
|
||||
auto job = createJob();
|
||||
Attributes attributes(1);
|
||||
HANDLE raw = job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &raw, sizeof(raw));
|
||||
auto result = object(env);
|
||||
set(env, result, "abiVersion", number(env, 1));
|
||||
set(env, result, "nodeApiVersion", number(env, 8));
|
||||
set(env, result, "architecture", string(env, std::string("x64")));
|
||||
set(env, result, "filesystem", filesystem);
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "atomicJobAssignment", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
Reference in New Issue
Block a user