From 3b1e58c9536de9428564b4c8ecac744b4a610cf0 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 05:18:52 +0900 Subject: [PATCH 1/4] feat: observe declared units through native identity --- src/process.cpp | 106 ++++++++++++++++++++++++++++++++++++++++++- src/unit_reference.h | 29 ++++++++++++ src/win32.h | 2 + 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/unit_reference.h diff --git a/src/process.cpp b/src/process.cpp index 33b9834..60d3f6b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -4,6 +4,7 @@ #include #include #include +#include // A program image that has been started. // @@ -20,6 +21,7 @@ // to construct it out of nothing. namespace { +bool observable_units(); constexpr okw_uptr kCommandLine = 32768; // this environment's own bound @@ -1279,7 +1281,7 @@ void kal_process_close(kal_process p) { // // BOUND_LIFETIME and STOP_REQUESTED were absent until 0.13 (K5, K3); GRANT_DIR // until 0.19.0 (openkal 0.23), which conveys a grant as an inherited handle. -kal_uintptr kal_process_props(void) { return +kal_uintptr kal_process_props(void) { return (observable_units() ? KAL_PROCESS_PROP_JOB_OBSERVE : 0u) | KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS | KAL_PROCESS_PROP_CHANNEL @@ -1293,3 +1295,105 @@ kal_uintptr kal_process_props(void) { return | KAL_PROCESS_PROP_GRANT_DIR; } // 0.23: a grant is an inherited handle } + +// Observable units (openkal 0.24): original native job handles, not a registry. +#include "unit_reference.h" +namespace { +using compare_objects_fn = BOOL (OKW_API*)(HANDLE,HANDLE); +compare_objects_fn unit_compare() { + return reinterpret_cast(okw::system_function(L"kernelbase.dll","CompareObjectHandles")); +} +bool observable_units() { return unit_compare() != nullptr; } +struct unit_identity { + kal_u64 owner = 0, birth = 0; char name[96]{}; +}; +bool unit_birth(kal_u64& birth) { + FILETIME created{}, ended{}, kernel{}, user{}; + if (!GetProcessTimes(GetCurrentProcess(),&created,&ended,&kernel,&user)) return false; + birth = (static_cast(created.dwHighDateTime) << 32) | created.dwLowDateTime; + return birth != 0; +} +bool unit_parse(const char* p, kal_uintptr n, unit_identity& u) { + if (!p || n == 0 || n > 256) return false; + unit_reference::reader r{p,n}; char kind[8]{}; + if (!r.token(kind,sizeof kind) || !unit_reference::same(kind,"W1") + || !r.number(u.owner) || !u.owner || !r.number(u.birth) || !u.birth + || !r.token(u.name,sizeof u.name) || r.at != n) return false; + static const char prefix[] = "Local\\openkal-unit-"; + for (kal_uintptr i = 0; i < sizeof prefix - 1; ++i) if (u.name[i] != prefix[i]) return false; + kal_uintptr at = sizeof prefix - 1; + for (kal_uintptr i = 0; i < 32; ++i) { + const char c = u.name[at + i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return false; + } + return u.name[at + 32] == 0; +} +HANDLE unit_open(const unit_identity& u) { + wchar_t name[96]{}; + for (kal_uintptr i = 0; u.name[i]; ++i) name[i] = static_cast(u.name[i]); + return OpenJobObjectW(4 /* JOB_OBJECT_QUERY */,FALSE,name); +} +bool unit_matches(HANDLE original, const unit_identity& u) { + const auto compare = unit_compare(); if (!compare) return false; + HANDLE named = unit_open(u); if (!named) return false; + const bool same = compare(original,named) != FALSE; CloseHandle(named); return same; +} +} +extern "C" { +int kal_process_job_create(kal_job* out, char* reference, kal_uintptr capacity, kal_uintptr* used) { + if (!out || !reference || !used) return kal_err_invalid; + if (capacity < 256) return kal_err_no_space; + if (!unit_compare()) return kal_err_not_supported; + unit_identity u; u.owner = GetCurrentProcessId(); + if (!unit_birth(u.birth)) return kal_err_not_supported; + unsigned char random[16]{}; + const int random_rc = kal_random_fill(random,sizeof random); if (random_rc != kal_ok) return random_rc; + static const char prefix[] = "Local\\openkal-unit-", hex[] = "0123456789abcdef"; + kal_uintptr at = 0; + for (; at < sizeof prefix - 1; ++at) u.name[at] = prefix[at]; + for (unsigned i = 0; i < 16; ++i) { u.name[at++] = hex[random[i] >> 4]; u.name[at++] = hex[random[i] & 15]; } + wchar_t name[96]{}; for (kal_uintptr i = 0; i < at; ++i) name[i] = u.name[i]; + SetLastError(0); HANDLE job = CreateJobObjectW(nullptr,name); + if (!job) return okw::translate_win32(GetLastError()); + if (GetLastError() == 183 /* ERROR_ALREADY_EXISTS */) { CloseHandle(job); return kal_err_again; } + unit_reference::writer w; w.text("W1;"); w.number(u.owner); w.next(); w.number(u.birth); w.next(); w.text(u.name); + if (!w.valid) { CloseHandle(job); return kal_err_no_space; } + if (!AssignProcessToJobObject(job,GetCurrentProcess())) { const auto error = GetLastError(); CloseHandle(job); return okw::translate_win32(error); } + const auto packed = okw::pack(job); + if (!packed) { CloseHandle(job); return kal_err_no_memory; } + out->h = packed; return unit_reference::copy(w,reference,capacity,used); +} +int kal_process_job_open(const char* reference, kal_uintptr length, kal_job* out) { + if (!out) return kal_err_invalid; + unit_identity u; if (!unit_parse(reference,length,u)) return kal_err_invalid; + if (!unit_compare()) return kal_err_not_supported; + HANDLE job = unit_open(u); if (!job) return okw::translate_win32(GetLastError()); + const auto packed = okw::pack(job); + if (!packed) { CloseHandle(job); return kal_err_no_memory; } + out->h = packed; return kal_ok; +} +int kal_process_job_observe(kal_job job, const char* reference, kal_uintptr length, kal_u32* state) { + if (!state) return kal_err_invalid; *state = KAL_JOB_UNKNOWN; + unit_identity u; if (!unit_parse(reference,length,u)) return kal_err_invalid; + HANDLE original = okw::unpack(job.h); if (!original) return kal_err_invalid; + if (!unit_matches(original,u)) return kal_ok; + JOB_ACCOUNTING_ usage{}; + if (!QueryInformationJobObject(original,JobObjectBasicAccountingInformation_,&usage,sizeof usage,nullptr)) return kal_ok; + *state = usage.ActiveProcesses == 0 ? KAL_JOB_EMPTY : KAL_JOB_OCCUPIED; + return kal_ok; +} +int kal_process_job_retire(kal_job job, const char* reference, kal_uintptr length, kal_u32* state) { + if (!state) return kal_err_invalid; *state = KAL_JOB_UNKNOWN; + unit_identity u; if (!unit_parse(reference,length,u)) return kal_err_invalid; + kal_u64 birth = 0; + if (u.owner != GetCurrentProcessId() || !unit_birth(birth) || birth != u.birth) return kal_err_permission; + HANDLE original = okw::unpack(job.h); if (!original) return kal_err_invalid; + if (!unit_matches(original,u)) return kal_ok; + BOOL member = FALSE; + if (!IsProcessInJob(GetCurrentProcess(),original,&member) || !member) return kal_ok; + JOB_ACCOUNTING_ usage{}; + if (!QueryInformationJobObject(original,JobObjectBasicAccountingInformation_,&usage,sizeof usage,nullptr)) return kal_ok; + *state = usage.ActiveProcesses == 1 ? KAL_JOB_EMPTY : usage.ActiveProcesses > 1 ? KAL_JOB_OCCUPIED : KAL_JOB_UNKNOWN; + return kal_ok; +} +} diff --git a/src/unit_reference.h b/src/unit_reference.h new file mode 100644 index 0000000..b6fadac --- /dev/null +++ b/src/unit_reference.h @@ -0,0 +1,29 @@ +// Counted, opaque unit references. This codec stores no process registry. +#pragma once +#include +namespace unit_reference { +struct writer { + char bytes[256]{}; kal_uintptr size = 0; bool valid = true; + void text(const char* p) { for (; *p; ++p) { if (size == sizeof bytes) { valid = false; return; } bytes[size++] = *p; } } + void number(kal_u64 n) { char b[24]; unsigned used = 0; do { b[used++] = char('0' + n % 10); n /= 10; } while (n); while (used) { char c[2]{b[--used],0}; text(c); } } + void next() { text(";"); } +}; +struct reader { + const char* bytes; kal_uintptr size, at = 0; + bool token(char* p, kal_uintptr capacity) { + kal_uintptr n = 0; + while (at < size && bytes[at] != ';') { if (n + 1 >= capacity || bytes[at] < 33 || bytes[at] > 126) return false; p[n++] = bytes[at++]; } + p[n] = 0; if (at < size) ++at; return n != 0; + } + bool number(kal_u64& n) { + n = 0; const auto begin = at; + while (at < size && bytes[at] != ';') { const auto c = bytes[at++]; if (c < '0' || c > '9' || n > (~kal_u64{0} - kal_u64(c - '0')) / 10) return false; n = n * 10 + kal_u64(c - '0'); } + if (at == begin) return false; if (at < size) ++at; return true; + } +}; +inline bool same(const char* a, const char* b) { for (; *a && *a == *b; ++a, ++b) {} return *a == *b; } +inline int copy(const writer& w, char* out, kal_uintptr capacity, kal_uintptr* used) { + if (!w.valid || capacity < w.size) return kal_err_no_space; + for (kal_uintptr i = 0; i < w.size; ++i) out[i] = w.bytes[i]; *used = w.size; return kal_ok; +} +} diff --git a/src/win32.h b/src/win32.h index 92f12a2..900b40c 100644 --- a/src/win32.h +++ b/src/win32.h @@ -301,6 +301,7 @@ extern "C" { OKW_IMPORT HANDLE OKW_API GetStdHandle(DWORD); OKW_IMPORT BOOL OKW_API CloseHandle(HANDLE); OKW_IMPORT DWORD OKW_API GetLastError(void); +OKW_IMPORT void OKW_API SetLastError(DWORD); OKW_IMPORT DWORD OKW_API GetFileType(HANDLE); OKW_IMPORT BOOL OKW_API SetHandleInformation(HANDLE, DWORD, DWORD); OKW_IMPORT BOOL OKW_API GetHandleInformation(HANDLE, DWORD*); @@ -383,6 +384,7 @@ OKW_IMPORT void OKW_API DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LI // does not end, because where a unit is a process group closing is releasing a // number. Not declaring the call is how that stays true by construction. OKW_IMPORT HANDLE OKW_API CreateJobObjectW(SECURITY_ATTRIBUTES*, LPCWSTR); +OKW_IMPORT HANDLE OKW_API OpenJobObjectW(DWORD, BOOL, LPCWSTR); OKW_IMPORT BOOL OKW_API AssignProcessToJobObject(HANDLE, HANDLE); OKW_IMPORT BOOL OKW_API TerminateJobObject(HANDLE, UINT); From 664628d0a01a38d028f59fd8c660891efa218679 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 06:10:48 +0900 Subject: [PATCH 2/4] fix: retain the continuation identity during retirement --- src/process.cpp | 23 ++++++++++++++++++++--- src/win32.h | 1 + 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index 60d3f6b..46e9035 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1382,17 +1382,34 @@ int kal_process_job_observe(kal_job job, const char* reference, kal_uintptr leng *state = usage.ActiveProcesses == 0 ? KAL_JOB_EMPTY : KAL_JOB_OCCUPIED; return kal_ok; } -int kal_process_job_retire(kal_job job, const char* reference, kal_uintptr length, kal_u32* state) { +int kal_process_job_retire(kal_job job, const char* reference, kal_uintptr length, + const kal_program_identity* continuation, kal_u32* state) { if (!state) return kal_err_invalid; *state = KAL_JOB_UNKNOWN; unit_identity u; if (!unit_parse(reference,length,u)) return kal_err_invalid; kal_u64 birth = 0; if (u.owner != GetCurrentProcessId() || !unit_birth(birth) || birth != u.birth) return kal_err_permission; HANDLE original = okw::unpack(job.h); if (!original) return kal_err_invalid; if (!unit_matches(original,u)) return kal_ok; + if (!continuation || !continuation->id || continuation->id > 0xffffffffu || !continuation->started_ns) return kal_ok; + DWORD caller_session = 0, destination_session = 0; + if (!ProcessIdToSessionId(GetCurrentProcessId(),&caller_session) + || !ProcessIdToSessionId(static_cast(continuation->id),&destination_session) + || caller_session != destination_session) return kal_ok; + kal_process anchor{}; + if (kal_process_open_id(continuation->id,&anchor) != kal_ok) return kal_ok; + kal_program_info info{}; info.self_size = sizeof info; + const int described = kal_process_info(anchor,KAL_PINFO_ID|KAL_PINFO_STARTED|KAL_PINFO_ENDED,&info); + if (described != kal_ok || info.ended || info.id != continuation->id + || (info.present & KAL_PINFO_STARTED) == 0 || info.started_ns != continuation->started_ns) {kal_process_close(anchor);return kal_ok;} BOOL member = FALSE; - if (!IsProcessInJob(GetCurrentProcess(),original,&member) || !member) return kal_ok; + if (!IsProcessInJob(GetCurrentProcess(),original,&member) || !member) {kal_process_close(anchor);return kal_ok;} JOB_ACCOUNTING_ usage{}; - if (!QueryInformationJobObject(original,JobObjectBasicAccountingInformation_,&usage,sizeof usage,nullptr)) return kal_ok; + if (!QueryInformationJobObject(original,JobObjectBasicAccountingInformation_,&usage,sizeof usage,nullptr)) {kal_process_close(anchor);return kal_ok;} + info={};info.self_size=sizeof info; + const int final_description=kal_process_info(anchor,KAL_PINFO_ID|KAL_PINFO_STARTED|KAL_PINFO_ENDED,&info); + kal_process_close(anchor); + if (final_description != kal_ok || info.ended || info.id != continuation->id + || (info.present & KAL_PINFO_STARTED) == 0 || info.started_ns != continuation->started_ns) return kal_ok; *state = usage.ActiveProcesses == 1 ? KAL_JOB_EMPTY : usage.ActiveProcesses > 1 ? KAL_JOB_OCCUPIED : KAL_JOB_UNKNOWN; return kal_ok; } diff --git a/src/win32.h b/src/win32.h index 900b40c..c4bbac5 100644 --- a/src/win32.h +++ b/src/win32.h @@ -498,6 +498,7 @@ OKW_IMPORT DWORD OKW_API GetModuleFileNameW(HANDLE, LPWSTR, DWORD); OKW_IMPORT HANDLE OKW_API OpenProcess(DWORD, BOOL, DWORD); OKW_IMPORT DWORD OKW_API GetProcessId(HANDLE); OKW_IMPORT DWORD OKW_API GetCurrentProcessId(void); +OKW_IMPORT BOOL OKW_API ProcessIdToSessionId(DWORD, DWORD*); OKW_IMPORT BOOL OKW_API GetProcessTimes(HANDLE, FILETIME*, FILETIME*, FILETIME*, FILETIME*); OKW_IMPORT void OKW_API ExitProcess(UINT); // A job that ends its member when the last handle to it closes (K5). From d09f316a35814c0cbde3a2add793659ea439c6a0 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 07:41:06 +0900 Subject: [PATCH 3/4] chore: release observable process units with OpenKal 0.24 --- .github/workflows/ci.yml | 4 ++-- README.md | 12 ++++++++++-- mcpp.toml | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a1dbbc..e513301 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,8 @@ on: default: "" env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} - MCPP_VERSION: 2026.8.27.1 - XLINGS_VERSION: v2026.8.17.2 + MCPP_VERSION: 2026.11.5.1 + XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' jobs: diff --git a/README.md b/README.md index 1bc7865..de96f34 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.23.0" +openkal = "0.24.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.19.0" +openkal-windows = "0.20.0" ``` Its purpose is as much to test the specification as to be used. openkal was @@ -15,6 +15,14 @@ drawn against systems that share an ancestry; a third implementation on a system that shares none of it is what turns "this interface is not Unix's shape" from a claim into an observation. +## Observable process units + +The process interface provides the OpenKal 0.24 declared-unit operations. +Observation retains the original named-job handle and queries its active +membership. A missing name after every handle is lost reports `unknown`. +Retirement checks the live continuation identity and requires that no external +member remains. Closing an observation handle does not terminate the unit. + ## What this environment settles **The file interface is not a Unix shape.** openkal has no global namespace of diff --git a/mcpp.toml b/mcpp.toml index b5138e2..300dc63 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.19.0" +version = "0.20.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -66,7 +66,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.23.0" +openkal = "0.24.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package From 07994ed5d8f7741474b42aa83bcda0256fc71bcc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 07:48:51 +0900 Subject: [PATCH 4/4] fix: export observable-unit native imports for cross links --- port/kernel32.def | 3 +++ 1 file changed, 3 insertions(+) diff --git a/port/kernel32.def b/port/kernel32.def index 86c4a46..edda9de 100644 --- a/port/kernel32.def +++ b/port/kernel32.def @@ -70,9 +70,11 @@ LoadLibraryW LocalFree MapViewOfFile MultiByteToWideChar +OpenJobObjectW OpenProcess OpenThread PeekNamedPipe +ProcessIdToSessionId QueryInformationJobObject QueryPerformanceCounter QueryPerformanceFrequency @@ -88,6 +90,7 @@ SetConsoleMode SetFilePointerEx SetHandleInformation SetInformationJobObject +SetLastError SetThreadPriority SetUnhandledExceptionFilter SetWaitableTimer