#include "authority.hpp" namespace rea { class Attributes { std::vector 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(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, bool parentReads = true) { 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(parentReads ? read.get() : write.get(), HANDLE_FLAG_INHERIT, 0), "Protect parent process pipe handle inheritance failed"); } std::unique_ptr spawnProcess(const std::wstring& command, const std::wstring& commandLine, const std::wstring& cwd, const std::vector& environment, bool protocolStdin) { const auto executable = ordinaryDriveSeparators(command); require(executable.size() >= 3 && executable[1] == L':' && executable[2] == L'\\', "Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER); auto result = std::make_unique(); result->job = createJob(); Handle stdoutWrite, stderrWrite; pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite); SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; Handle input; if (protocolStdin) pipe(input, result->stdinWrite, false); else { input.reset(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 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(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 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 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 line(commandLine.begin(), commandLine.end()); line.push_back(L'\0'); PROCESS_INFORMATION process{}; require(CreateProcessW(executable.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 tokenUser(tokenSize); require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize), "Read suspended process user identity failed", command); TOKEN_OWNER owner{reinterpret_cast(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 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(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 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 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(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(user.data())->User.Sid, reinterpret_cast(owner.data())->Owner))); set(env, result, "inJob", boolean(env, assigned != FALSE)); return result; } struct InputWork { napi_async_work work = nullptr; napi_deferred deferred = nullptr; napi_ref processReference = nullptr; Process* process; Handle pipe; std::vector bytes; size_t written = 0; std::unique_ptr failure; explicit InputWork(Process& process) : process(&process) {} }; static void writeInput(napi_env, void* pointer) { auto& work = *static_cast(pointer); try { // Anonymous pipe writes may block. Keep them off the JS thread so output // polling and job termination remain live while a child stops reading. while (work.written < work.bytes.size()) { DWORD written = 0; const auto count = static_cast(std::min(65536, work.bytes.size() - work.written)); require(WriteFile(work.pipe.get(), work.bytes.data() + work.written, count, &written, nullptr), "Write owned process input failed"); require(written > 0, "Owned process input write made no progress", L"", ERROR_WRITE_FAULT); work.written += written; } } catch (const Failure& failure) { work.failure = std::make_unique(failure); } catch (const std::exception& failure) { work.failure = std::make_unique(failure.what(), L"", ERROR_GEN_FAILURE); } } static void completeInput(napi_env env, napi_status status, void* pointer) { std::unique_ptr work(static_cast(pointer)); work->process->inputPending = false; if (work->failure) napi_reject_deferred(env, work->deferred, failureValue(env, *work->failure)); else if (status != napi_ok) napi_reject_deferred(env, work->deferred, failureValue(env, Failure("Owned process input worker was cancelled", L"", ERROR_OPERATION_ABORTED))); else napi_resolve_deferred(env, work->deferred, number(env, work->written)); napi_delete_reference(env, work->processReference); napi_delete_async_work(env, work->work); } static napi_value inputWrite(napi_env env, napi_value owner, Process& process, napi_value input) { require(process.stdinWrite.valid(), "Owned process has no writable input", L"", ERROR_BROKEN_PIPE); require(!process.inputPending, "Owned process input already has a pending write", L"", ERROR_BUSY); auto work = std::make_unique(process); void* bytes = nullptr; size_t size = 0; bool buffer = false; check(napi_is_buffer(env, input, &buffer)); require(buffer, "Owned process input must be a Buffer", L"", ERROR_INVALID_PARAMETER); check(napi_get_buffer_info(env, input, &bytes, &size)); if (size > 0) work->bytes.assign(static_cast(bytes), static_cast(bytes) + size); HANDLE pipe; require(DuplicateHandle(GetCurrentProcess(), process.stdinWrite.get(), GetCurrentProcess(), &pipe, 0, FALSE, DUPLICATE_SAME_ACCESS), "Retain owned process input handle failed"); work->pipe.reset(pipe); napi_value promise; check(napi_create_promise(env, &work->deferred, &promise)); check(napi_create_reference(env, owner, 1, &work->processReference)); try { check(napi_create_async_work(env, nullptr, string(env, std::string("REA owned process input")), writeInput, completeInput, work.get(), &work->work)); check(napi_queue_async_work(env, work->work)); process.inputPending = true; } catch (...) { if (work->work) napi_delete_async_work(env, work->work); napi_delete_reference(env, work->processReference); throw; } work.release(); return promise; } napi_value processCall(napi_env env, const std::wstring& operation, const std::vector& 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 || args.size() == 5, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER); bool protocolStdin = false; if (args.size() == 5) check(napi_get_value_bool(env, args[4], &protocolStdin)); std::vector 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, protocolStdin); 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; } if (operation == L"process_stdin_write") { require(args.size() == 2, "Wrong native process input argument count", L"", ERROR_INVALID_PARAMETER); return inputWrite(env, args[0], static_cast(resource(env, args[0], Kind::Process)), args[1]); } require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER); auto& process = static_cast(resource(env, args[0], Kind::Process)); if (operation == L"process_stdin_close") { process.stdinWrite.reset(); return null(env); } 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.stdinWrite.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)); set(env, result, "protocolStdin", boolean(env, true)); return result; } } // namespace rea