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:
N0zoM1z0
2026-10-06 16:53:33 +08:00
committed by GitHub
parent cf061159ab
commit 26881da76a
65 changed files with 4091 additions and 356 deletions
+102
View File
@@ -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.
+123
View File
@@ -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)
+120
View File
@@ -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
+531
View File
@@ -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
+271
View File
@@ -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