Files
orca/config/patches/@vscode__windows-process-tree@0.8.0.patch
T
OrcaWinandm4air 6d1a97ef98 fix(ssh): launch the Windows relay outside sshd's job so standard users work (#24224)
* fix(ssh): launch the Windows relay outside sshd's job without WMI

Win32-OpenSSH kills a session's job on close but allows breakaway. relay.js
gains a one-shot launcher mode that starts the detached relay with
CREATE_BREAKAWAY_FROM_JOB through the staged process-tree addon, so a standard
user no longer needs a WMI Remote Enable grant. WMI stays as the fallback for a
relay without the addon, and a refusal there is named. The Windows SSH-host
lanes drop their WMI grant and assert the breakaway route and adoption.

* fix(ssh): find runtime holds without WMI on a standard-user Windows host

The store GC read held runtimes through Get-CimInstance Win32_Process, which
WMI refuses to a standard user's SSH logon, so the pass kept every runtime.
On a refusal it now reads this account's own process image paths through
Get-Process.

* build(relay): ship the Windows relay launcher addon in every desktop package

macOS and Linux packages carried Windows relays without windows-process-tree.node,
so a legacy-runtime relay they uploaded to a Windows SSH host could not launch
outside sshd's job and fell back to WMI, which a standard user is refused.

A reusable Windows job now compiles the x64 and arm64 addons once and uploads
them; release-cut, release-mac-build, and the hourly/daily/adhoc mac builds
download them before build:release and require both arches. Staging now rejects
a binary with the wrong PE machine, the ReadProcessMemory import, or no
spawnOutsideJob export, so a stale pre-launcher build cannot ship.

* ci(ssh): run the Windows SSH-host lanes when the relay process-tree build scripts change

The staging and gyp-rebuild scripts decide which windows-process-tree addon the
relay ships, so a change to either must re-prove the Windows host cells.

* test(ci): find the mac orcad-template download by artifact name

The release mac job now also downloads the relay Windows process-tree addons, so
the first download-artifact step is no longer the template's.

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 05:32:09 -07:00

641 lines
24 KiB
Diff

diff --git a/binding.gyp b/binding.gyp
index 11a5e71..1b2e74b 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -3,7 +3,6 @@
{
"target_name": "windows_process_tree",
"dependencies": [
- "<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
"conditions": [
['OS=="win"', {
@@ -12,15 +11,15 @@
"src/cpu_worker.cc",
"src/process.cc",
"src/process_worker.cc",
- "src/process_commandline.cc"
+ "src/process_commandline.cc",
+ "src/process_launch.cc"
],
- "include_dirs": [],
+ "include_dirs": ["deps/node-addon-api"],
+ "defines": ["NAPI_CPP_EXCEPTIONS", "_HAS_EXCEPTIONS=1"],
"libraries": [ 'psapi.lib' ],
- "msvs_configuration_attributes": {
- "SpectreMitigation": "Spectre"
- },
"msvs_settings": {
"VCCLCompilerTool": {
+ "ExceptionHandling": 1,
"AdditionalOptions": [
"/guard:cf",
"/sdl",
diff --git a/lib/index.js b/lib/index.js
index e586cd6..0ee62c3 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -7,11 +7,14 @@
exports.getAllProcesses = exports.getProcessTree = exports.getProcessCpuUsage = exports.getProcessList = exports.filterProcessList = exports.buildProcessTree = exports.ProcessDataFlag = void 0;
const util_1 = require("util");
const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined;
+exports.getProcessCreationTime = native === undefined ? undefined : native.getProcessCreationTime;
+exports.supportedProcessDataFlags = native === undefined ? undefined : native.supportedProcessDataFlags;
var ProcessDataFlag;
(function (ProcessDataFlag) {
ProcessDataFlag[ProcessDataFlag["None"] = 0] = "None";
ProcessDataFlag[ProcessDataFlag["Memory"] = 1] = "Memory";
ProcessDataFlag[ProcessDataFlag["CommandLine"] = 2] = "CommandLine";
+ ProcessDataFlag[ProcessDataFlag["CreationTime"] = 4] = "CreationTime";
})(ProcessDataFlag = exports.ProcessDataFlag || (exports.ProcessDataFlag = {}));
// requestInProgress is used for any function that uses CreateToolhelp32Snapshot, as multiple calls
// to this cannot be done at the same time.
@@ -66,11 +69,12 @@
// • the properties are inlined/splatted
// • the 'ppid' field is omitted
// • the depth of the tree is limited by `maxDepth`
- const buildNode = ({ info: { pid, name, memory, commandLine }, children }, depth) => ({
+ const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }, depth) => ({
pid,
name,
memory,
commandLine,
+ creationTimeMs,
children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [],
});
return buildNode(root, maxDepth);
diff --git a/lib/index.ts b/lib/index.ts
index 7b53aad..284dae1 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -6,12 +6,16 @@
import { promisify } from 'util';
const native = process.platform === 'win32' ? require('../build/Release/windows_process_tree.node') : undefined;
+export const getProcessCreationTime: ((pid: number) => number | undefined) | undefined = native?.getProcessCreationTime;
+/** The flag bits this compiled addon reports; undefined off win32. */
+export const supportedProcessDataFlags: number | undefined = native?.supportedProcessDataFlags;
import { IProcessInfo, IProcessTreeNode, IProcessCpuInfo } from '@vscode/windows-process-tree';
export enum ProcessDataFlag {
None = 0,
Memory = 1,
- CommandLine = 2
+ CommandLine = 2,
+ CreationTime = 4
}
type RequestCallback = (processList: IProcessInfo[]) => void;
@@ -81,11 +85,12 @@
// • the properties are inlined/splatted
// • the 'ppid' field is omitted
// • the depth of the tree is limited by `maxDepth`
- const buildNode = ({ info: { pid, name, memory, commandLine }, children }: IProcessInfoNode, depth: number): IProcessTreeNode => ({
+ const buildNode = ({ info: { pid, name, memory, commandLine, creationTimeMs }, children }: IProcessInfoNode, depth: number): IProcessTreeNode => ({
pid,
name,
memory,
commandLine,
+ creationTimeMs,
children: depth > 0 ? children.map(c => buildNode(c, depth - 1)) : [],
});
diff --git a/src/addon.cc b/src/addon.cc
index 5253960..f146490 100644
--- a/src/addon.cc
+++ b/src/addon.cc
@@ -6,6 +6,7 @@
#include <napi.h>
#include "cpu_worker.h"
#include "process_worker.h"
+#include "process_launch.h"
void GetProcessList(const Napi::CallbackInfo& args) {
Napi::Env env(args.Env());
@@ -50,9 +51,33 @@
worker->Queue();
}
+Napi::Value ReadProcessCreationTime(const Napi::CallbackInfo& args) {
+ Napi::Env env(args.Env());
+ if (args.Length() != 1 || !args[0].IsNumber()) {
+ return env.Undefined();
+ }
+ const double pid = args[0].As<Napi::Number>().DoubleValue();
+ if (!(pid >= 1 && pid <= MAXDWORD) || pid != static_cast<DWORD>(pid)) {
+ return env.Undefined();
+ }
+ ProcessInfo pinfo{};
+ pinfo.pid = static_cast<DWORD>(pid);
+ GetProcessCreationTime(pinfo);
+ if (pinfo.creationTimeMs == 0) {
+ return env.Undefined();
+ }
+ return Napi::Number::New(env, static_cast<double>(pinfo.creationTimeMs));
+}
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
+ exports.Set("getProcessCreationTime", Napi::Function::New(env, ReadProcessCreationTime));
exports.Set("getProcessList", Napi::Function::New(env, GetProcessList));
exports.Set("getProcessCpuUsage", Napi::Function::New(env, GetProcessCpuUsage));
+ exports.Set("spawnOutsideJob", Napi::Function::New(env, SpawnOutsideJob));
+ // Lets a caller prove THIS BINARY understands CREATIONTIME. The JS enum is
+ // patched source and says nothing about what the .node was compiled from.
+ exports.Set("supportedProcessDataFlags",
+ Napi::Number::New(env, MEMORY | COMMANDLINE | CREATIONTIME));
return exports;
}
diff --git a/src/process.cc b/src/process.cc
index ad63727..22a4742 100644
--- a/src/process.cc
+++ b/src/process.cc
@@ -21,7 +21,8 @@
if (Process32First(snapshot_handle, &process_entry)) {
do {
if (process_entry.th32ProcessID != 0) {
- ProcessInfo pinfo;
+ // Value-initialize: `memory` is otherwise stack garbage when the flag is unset.
+ ProcessInfo pinfo{};
pinfo.pid = process_entry.th32ProcessID;
pinfo.ppid = process_entry.th32ParentProcessID;
@@ -33,23 +34,51 @@
GetProcessCommandLine(pinfo);
}
+ if (CREATIONTIME & process_data_flags) {
+ GetProcessCreationTime(pinfo);
+ }
+
strcpy(pinfo.name, process_entry.szExeFile);
process_info.push_back(std::move(pinfo));
process_count++;
}
- } while (process_count < 1024 && Process32Next(snapshot_handle, &process_entry));
+ } while (Process32Next(snapshot_handle, &process_entry));
}
CloseHandle(snapshot_handle);
return process_count;
}
+void GetProcessCreationTime(ProcessInfo& process_info) {
+ HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_info.pid);
+ if (hProcess == NULL) {
+ return;
+ }
+
+ FILETIME creationTime, exitTime, kernelTime, userTime;
+ if (GetProcessTimes(hProcess, &creationTime, &exitTime, &kernelTime, &userTime)) {
+ ULARGE_INTEGER timestamp;
+ timestamp.LowPart = creationTime.dwLowDateTime;
+ timestamp.HighPart = creationTime.dwHighDateTime;
+ constexpr ULONGLONG WINDOWS_EPOCH_OFFSET_100NS = 116444736000000000ULL;
+ constexpr ULONGLONG HUNDRED_NS_PER_MILLISECOND = 10000ULL;
+ if (timestamp.QuadPart >= WINDOWS_EPOCH_OFFSET_100NS) {
+ process_info.creationTimeMs =
+ (timestamp.QuadPart - WINDOWS_EPOCH_OFFSET_100NS) / HUNDRED_NS_PER_MILLISECOND;
+ }
+ }
+
+ CloseHandle(hProcess);
+}
+
void GetProcessMemoryUsage(ProcessInfo& process_info) {
DWORD pid = process_info.pid;
HANDLE hProcess;
PROCESS_MEMORY_COUNTERS pmc;
- hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
+ // PROCESS_VM_READ is never used here -- GetProcessMemoryInfo reads counters the
+ // kernel keeps, not the address space -- and acquiring it is what EDR scores.
+ hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProcess == NULL) {
return;
@@ -81,7 +110,8 @@
DWORD pid = cpu_info.pid;
HANDLE hProcess;
- hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid);
+ // GetProcessTimes needs no more than PROCESS_QUERY_LIMITED_INFORMATION.
+ hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
if (hProcess == NULL) {
return;
diff --git a/src/process.h b/src/process.h
index 3c9b852..72e1648 100644
--- a/src/process.h
+++ b/src/process.h
@@ -22,18 +22,22 @@
DWORD ppid;
DWORD memory; // Reported in bytes
std::string commandLine;
+ ULONGLONG creationTimeMs;
};
enum ProcessDataFlags {
NONE = 0,
MEMORY = 1,
- COMMANDLINE = 2
+ COMMANDLINE = 2,
+ CREATIONTIME = 4
};
uint32_t GetRawProcessList(std::vector<ProcessInfo>& process_info, DWORD flags);
void GetProcessMemoryUsage(ProcessInfo& process_info);
+void GetProcessCreationTime(ProcessInfo& process_info);
+
void GetCpuUsage(Cpu& cpu_info, bool first_run);
#endif // SRC_PROCESS_H_
diff --git a/src/process_commandline.cc b/src/process_commandline.cc
index 716051d..25907c0 100644
--- a/src/process_commandline.cc
+++ b/src/process_commandline.cc
@@ -7,61 +7,119 @@
#include "process_commandline.h"
#include <windows.h>
#include <winternl.h>
-#include <iostream>
+#include <vector>
-bool GetProcessCommandLine(ProcessInfo& process_info) {
- HINSTANCE ntdll = GetModuleHandleW(L"ntdll.dll");
+namespace {
+
+// Windows 8.1 and later hand back a process's command line as a UNICODE_STRING
+// the kernel builds, needing only PROCESS_QUERY_LIMITED_INFORMATION.
+//
+// There is deliberately no PEB fallback. Reading the command line out of the
+// target's address space -- opening it for VM reads and then chaining
+// memory reads across every pid on a timer -- is the credential-dumping
+// primitive this reader exists to not perform, so it is absent from the binary
+// rather than one anomalous NTSTATUS away. Electron's floor is Windows 10, so
+// every OS Orca supports has this class; if a hooked ntdll refuses it anyway,
+// the command line comes back empty, which callers already handle, instead of
+// silently reinstating the primitive on exactly the instrumented machines this
+// reader was written for.
+const ULONG kProcessCommandLineInformation = 60;
+
+const NTSTATUS kStatusInfoLengthMismatch = static_cast<NTSTATUS>(0xC0000004L);
+const NTSTATUS kStatusBufferTooSmall = static_cast<NTSTATUS>(0xC0000023L);
+
+// A command line is a UNICODE_STRING, whose Length is a USHORT, so the kernel
+// can never need more than the header plus 64 KiB. Refusing anything larger
+// keeps a bogus size from throwing bad_alloc out of a scan that has already
+// walked most of the table.
+const ULONG kMaxCommandLineBytes = sizeof(UNICODE_STRING) + 0xFFFF + sizeof(wchar_t);
+
+// winternl.h's PROCESSINFOCLASS does not name class 60 and its enumerator range
+// stops far short of it, so the class travels as a ULONG rather than a cast enum.
+typedef NTSTATUS(NTAPI* NtQueryInformationProcessFn)(HANDLE, ULONG, PVOID, ULONG, PULONG);
+
+// ntdll ships no import library for this entry point; it has to be resolved.
+NtQueryInformationProcessFn ResolveNtQueryInformationProcess() {
+ HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
if (!ntdll) {
+ return nullptr;
+ }
+ return reinterpret_cast<NtQueryInformationProcessFn>(
+ GetProcAddress(ntdll, "NtQueryInformationProcess"));
+}
+
+NtQueryInformationProcessFn NtQueryInformationProcessEntry() {
+ static NtQueryInformationProcessFn entry = ResolveNtQueryInformationProcess();
+ return entry;
+}
+
+bool StoreCommandLineUtf8(ProcessInfo& process_info, const wchar_t* data, size_t wide_length) {
+ if (wide_length == 0) {
+ return false;
+ }
+ int length = static_cast<int>(wide_length);
+ int charcount = WideCharToMultiByte(CP_UTF8, 0, data, length, NULL, 0, NULL, NULL);
+ if (!charcount) {
return false;
}
+ process_info.commandLine.resize(static_cast<size_t>(charcount));
+ WideCharToMultiByte(CP_UTF8, 0, data, length, &process_info.commandLine[0], charcount, NULL,
+ NULL);
+ return true;
+}
+
+} // namespace
- decltype(NtQueryInformationProcess)* nt_query_information_process =
- reinterpret_cast<decltype(NtQueryInformationProcess)*>(
- GetProcAddress(ntdll, "NtQueryInformationProcess"));
+bool GetProcessCommandLine(ProcessInfo& process_info) {
+ NtQueryInformationProcessFn query = NtQueryInformationProcessEntry();
+ if (!query) {
+ return false;
+ }
- if (!nt_query_information_process) {
+ HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_info.pid);
+ if (process == NULL) {
return false;
}
- PROCESS_BASIC_INFORMATION pbi{};
- PEB peb = {NULL};
- RTL_USER_PROCESS_PARAMETERS process_parameters = {NULL};
+ ULONG size = 0;
+ NTSTATUS status = query(process, kProcessCommandLineInformation, nullptr, 0, &size);
+ if (NT_SUCCESS(status)) {
+ // Nothing was written, so there is no command line to read.
+ CloseHandle(process);
+ return false;
+ }
+ if (status != kStatusInfoLengthMismatch && status != kStatusBufferTooSmall) {
+ CloseHandle(process);
+ return false;
+ }
+ if (size < sizeof(UNICODE_STRING) || size > kMaxCommandLineBytes) {
+ CloseHandle(process);
+ return false;
+ }
- // Get process handle
- DWORD pid = process_info.pid;
- HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
- if (hProcess == INVALID_HANDLE_VALUE) {
+ std::vector<unsigned char> buffer(size);
+ status = query(process, kProcessCommandLineInformation, &buffer[0], size, &size);
+ CloseHandle(process);
+ if (!NT_SUCCESS(status)) {
return false;
}
- // Get Process Environment Block (PEB)
- NTSTATUS status = nt_query_information_process(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), nullptr);
- if (NT_SUCCESS(status) && pbi.PebBaseAddress) {
- // Read PEB
- if (ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), nullptr)) {
- // Read the processs parameters
- if (ReadProcessMemory(hProcess, peb.ProcessParameters, &process_parameters, sizeof(RTL_USER_PROCESS_PARAMETERS), nullptr)) {
- if (process_parameters.CommandLine.Length > 0) {
- std::wstring buffer;
- buffer.resize(process_parameters.CommandLine.Length / sizeof(wchar_t));
- if (ReadProcessMemory(hProcess, process_parameters.CommandLine.Buffer, &buffer[0], process_parameters.CommandLine.Length, nullptr)) {
- int wide_length = static_cast<int>(buffer.length());
- int charcount = WideCharToMultiByte(CP_UTF8, 0, buffer.data(), wide_length,
- NULL, 0, NULL, NULL);
- if (charcount) {
- process_info.commandLine.resize(static_cast<size_t>(charcount));
- WideCharToMultiByte(CP_UTF8, 0, buffer.data(), wide_length,
- &process_info.commandLine[0], charcount,
- NULL, NULL);
- }
- CloseHandle(hProcess);
- return true;
- }
- }
- }
- }
+ // Header and characters arrive in one allocation, but treat the header as
+ // untrusted: a hooked ntdll is the case this reader is written for, and an
+ // unchecked Buffer/Length here would be an over-read encoded straight into JS.
+ // Bound against buffer.size(), never `size` -- the second query overwrote it.
+ const UNICODE_STRING* command_line = reinterpret_cast<const UNICODE_STRING*>(&buffer[0]);
+ const unsigned char* begin = &buffer[0];
+ const unsigned char* end = begin + buffer.size();
+ const unsigned char* chars = reinterpret_cast<const unsigned char*>(command_line->Buffer);
+ if (chars == nullptr || chars < begin + sizeof(UNICODE_STRING) || chars > end ||
+ command_line->Length > static_cast<ULONG>(end - chars)) {
+ return false;
}
- CloseHandle(hProcess);
- return false;
+ // True only when a command line was actually stored, so "empty" and "not
+ // recovered" stay the same answer they were before this reader replaced the
+ // PEB read. `src/process.cc` discards the result either way.
+ return StoreCommandLineUtf8(process_info, command_line->Buffer,
+ command_line->Length / sizeof(wchar_t));
}
diff --git a/src/process_launch.cc b/src/process_launch.cc
new file mode 100644
index 0000000..e3141c5
--- /dev/null
+++ b/src/process_launch.cc
@@ -0,0 +1,140 @@
+// Orca: start a detached process outside the caller's job object.
+//
+// Win32-OpenSSH puts every session's shell in a job with KILL_ON_JOB_CLOSE, so
+// anything a session starts dies when the session ends. The same job carries
+// BREAKAWAY_OK, so CREATE_BREAKAWAY_FROM_JOB lets a standard user start a
+// process that outlives it -- which Node cannot ask for: libuv never passes it.
+
+#include "process_launch.h"
+
+#include <windows.h>
+#include <string>
+#include <vector>
+
+namespace {
+
+class OwnedHandle {
+ public:
+ explicit OwnedHandle(HANDLE handle) : handle_(handle) {}
+ ~OwnedHandle() {
+ if (valid()) {
+ CloseHandle(handle_);
+ }
+ }
+ OwnedHandle(const OwnedHandle&) = delete;
+ OwnedHandle& operator=(const OwnedHandle&) = delete;
+ bool valid() const { return handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE; }
+ HANDLE get() const { return handle_; }
+
+ private:
+ HANDLE handle_;
+};
+
+std::wstring ToWide(const Napi::Value& value) {
+ const std::u16string text = value.As<Napi::String>().Utf16Value();
+ return std::wstring(text.begin(), text.end());
+}
+
+HANDLE OpenInheritable(const std::wstring& path, DWORD access, DWORD disposition) {
+ SECURITY_ATTRIBUTES attributes{};
+ attributes.nLength = sizeof(attributes);
+ attributes.bInheritHandle = TRUE;
+ return CreateFileW(path.c_str(), access,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ &attributes, disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
+}
+
+Napi::Object Failure(Napi::Env env, const char* reason, const char* step, DWORD code) {
+ Napi::Object result = Napi::Object::New(env);
+ result.Set("ok", false);
+ result.Set("reason", reason);
+ result.Set("step", step);
+ result.Set("code", static_cast<double>(code));
+ return result;
+}
+
+} // namespace
+
+// spawnOutsideJob(application, commandLine, cwd, stdoutPath, stderrPath)
+Napi::Value SpawnOutsideJob(const Napi::CallbackInfo& args) {
+ Napi::Env env(args.Env());
+ if (args.Length() != 5) {
+ throw Napi::TypeError::New(env, "spawnOutsideJob expects five string arguments.");
+ }
+ for (size_t index = 0; index < args.Length(); index++) {
+ if (!args[index].IsString()) {
+ throw Napi::TypeError::New(env, "spawnOutsideJob expects five string arguments.");
+ }
+ }
+ const std::wstring application = ToWide(args[0]);
+ const std::wstring command_line = ToWide(args[1]);
+ const std::wstring cwd = ToWide(args[2]);
+ // CreateProcessW may write into the command-line buffer.
+ std::vector<wchar_t> command_buffer(command_line.begin(), command_line.end());
+ command_buffer.push_back(L'\0');
+
+ OwnedHandle input(OpenInheritable(L"NUL", GENERIC_READ, OPEN_EXISTING));
+ if (!input.valid()) {
+ return Failure(env, "failed", "open-stdin", GetLastError());
+ }
+ OwnedHandle output(OpenInheritable(ToWide(args[3]), GENERIC_WRITE, CREATE_ALWAYS));
+ if (!output.valid()) {
+ return Failure(env, "failed", "open-stdout", GetLastError());
+ }
+ OwnedHandle error_output(OpenInheritable(ToWide(args[4]), GENERIC_WRITE, CREATE_ALWAYS));
+ if (!error_output.valid()) {
+ return Failure(env, "failed", "open-stderr", GetLastError());
+ }
+
+ // Inherit exactly these three: an inherited SSH channel pipe would hold the
+ // launching session open for the relay's whole life.
+ HANDLE inherited[3] = {input.get(), output.get(), error_output.get()};
+ SIZE_T attribute_size = 0;
+ InitializeProcThreadAttributeList(nullptr, 1, 0, &attribute_size);
+ std::vector<unsigned char> attribute_storage(attribute_size);
+ auto* attribute_list =
+ reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attribute_storage.data());
+ if (!InitializeProcThreadAttributeList(attribute_list, 1, 0, &attribute_size)) {
+ return Failure(env, "failed", "attribute-list", GetLastError());
+ }
+ if (!UpdateProcThreadAttribute(attribute_list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ inherited, sizeof(inherited), nullptr, nullptr)) {
+ const DWORD code = GetLastError();
+ DeleteProcThreadAttributeList(attribute_list);
+ return Failure(env, "failed", "handle-list", code);
+ }
+
+ STARTUPINFOEXW startup{};
+ startup.StartupInfo.cb = sizeof(startup);
+ startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
+ startup.StartupInfo.hStdInput = input.get();
+ startup.StartupInfo.hStdOutput = output.get();
+ startup.StartupInfo.hStdError = error_output.get();
+ startup.lpAttributeList = attribute_list;
+ PROCESS_INFORMATION info{};
+ // CREATE_NO_WINDOW keeps one hidden console for the relay's console
+ // children, as the cmd.exe that WMI used to start it did.
+ const DWORD flags = CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_PROCESS_GROUP |
+ CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT;
+ const BOOL created = CreateProcessW(
+ application.c_str(), command_buffer.data(), nullptr, nullptr, TRUE, flags, nullptr,
+ cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &info);
+ const DWORD create_error = created ? ERROR_SUCCESS : GetLastError();
+ DeleteProcThreadAttributeList(attribute_list);
+ if (!created) {
+ BOOL caller_in_job = FALSE;
+ IsProcessInJob(GetCurrentProcess(), nullptr, &caller_in_job);
+ const bool denied = create_error == ERROR_ACCESS_DENIED && caller_in_job;
+ return Failure(env, denied ? "breakaway-denied" : "failed", "create-process", create_error);
+ }
+ BOOL child_in_job = FALSE;
+ IsProcessInJob(info.hProcess, nullptr, &child_in_job);
+ CloseHandle(info.hThread);
+ CloseHandle(info.hProcess);
+
+ Napi::Object result = Napi::Object::New(env);
+ result.Set("ok", true);
+ result.Set("pid", static_cast<double>(info.dwProcessId));
+ result.Set("inJob", child_in_job != FALSE);
+ return result;
+}
diff --git a/src/process_launch.h b/src/process_launch.h
new file mode 100644
index 0000000..88872a1
--- /dev/null
+++ b/src/process_launch.h
@@ -0,0 +1,10 @@
+// Orca: start a detached process outside the caller's job object.
+
+#ifndef SRC_PROCESS_LAUNCH_H_
+#define SRC_PROCESS_LAUNCH_H_
+
+#include <napi.h>
+
+Napi::Value SpawnOutsideJob(const Napi::CallbackInfo& args);
+
+#endif // SRC_PROCESS_LAUNCH_H_
diff --git a/src/process_worker.cc b/src/process_worker.cc
index f59559d..1d61c87 100644
--- a/src/process_worker.cc
+++ b/src/process_worker.cc
@@ -43,6 +43,11 @@
Napi::String::New(env, pinfo.commandLine));
}
+ if ((CREATIONTIME & process_data_flags_) && pinfo.creationTimeMs != 0) {
+ object.Set("creationTimeMs",
+ Napi::Number::New(env, static_cast<double>(pinfo.creationTimeMs)));
+ }
+
result.Set(i, object);
}
diff --git a/typings/windows-process-tree.d.ts b/typings/windows-process-tree.d.ts
index 70e242b..b1d0a53 100644
--- a/typings/windows-process-tree.d.ts
+++ b/typings/windows-process-tree.d.ts
@@ -7,9 +7,18 @@
export enum ProcessDataFlag {
None = 0,
Memory = 1,
- CommandLine = 2
+ CommandLine = 2,
+ CreationTime = 4
}
+ /**
+ * The flag bits the compiled addon actually understands, or undefined off
+ * win32. `ProcessDataFlag` above is source; this is what the binary reports,
+ * so it is the only way to tell a patched build from a stale prebuilt.
+ */
+ export const supportedProcessDataFlags: number | undefined;
+ export const getProcessCreationTime: ((pid: number) => number | undefined) | undefined;
+
export interface IProcessInfo {
pid: number;
ppid: number;
@@ -24,6 +33,9 @@
* The string returned is at most 512 chars, strings exceeding this length are truncated.
*/
commandLine?: string;
+
+ /** Process creation time in Unix milliseconds. */
+ creationTimeMs?: number;
}
export interface IProcessCpuInfo extends IProcessInfo {
@@ -35,6 +47,7 @@
name: string;
memory?: number;
commandLine?: string;
+ creationTimeMs?: number;
children: IProcessTreeNode[];
}