From ead3b389305b68d538f96d01d75fe9300992e155 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 08:28:54 +0900 Subject: [PATCH 1/4] 0.14.1 --- a copy of the space can start a thread kal_space_start made the copy with the duplication primitive alone, and this system's fork does more: around the primitive the system library takes its locks and, in the copy, makes its state true of the new process. Without that the copy's first thread failed with EAGAIN (mcpplibs/openkal-musl#55). The three preparations fork itself calls are now called around the primitive, under names no C library defines, and port/libSystem.tbd names them. tests/conformance_space_task.cpp starts a context, joins it and starts one with a least stack size in a copy, three times. --- README.md | 2 +- mcpp.toml | 2 +- port/libSystem.tbd | 13 +++++-- src/space.cpp | 24 +++++++++++- tests/conformance_space_task.cpp | 64 ++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 tests/conformance_space_task.cpp diff --git a/README.md b/README.md index 003f8a7..e1c350e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ written on the kernel's own calls. openkal = "0.16.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.14.0" +openkal-macos = "0.14.1" ``` Its purpose is as much to test the specification as to be used. A specification diff --git a/mcpp.toml b/mcpp.toml index d9b8957..2ee8e69 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.14.0" +version = "0.14.1" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" diff --git a/port/libSystem.tbd b/port/libSystem.tbd index d74f948..d99dfc4 100644 --- a/port/libSystem.tbd +++ b/port/libSystem.tbd @@ -28,10 +28,13 @@ targets: [ arm64-macos, x86_64-macos ] install-name: '/usr/lib/libSystem.B.dylib' current-version: 1351.0.0 # -# FIVE NAMES ARE CALLED AND ONE IS NOT. +# EIGHT NAMES ARE CALLED AND ONE IS NOT. # -# The first four are names this implementation CALLS, and the enumeration above -# found them. The three that 0.13.0 added answer the stack of the calling +# The first five are names this implementation CALLS, and the enumeration above +# found them. The three `_libSystem_atfork_*' that 0.14.1 added are the +# preparation `fork' performs around the primitive, which a duplicate made by +# kal_space_start needs before it can start a thread (mcpplibs/openkal-musl#55). +# The three that 0.13.0 added answer the stack of the calling # thread (openkal 0.15, clause 11 entry 21), and they follow the same rule as # the two before them: this system's library defines them and no C library in # this ecosystem does, which is what makes them reachable from an @@ -54,7 +57,9 @@ current-version: 1351.0.0 # evidence about a format. exports: - targets: [ arm64-macos, x86_64-macos ] - symbols: [ _clock_gettime_nsec_np, _pthread_create_from_mach_thread, + symbols: [ __libSystem_atfork_child, __libSystem_atfork_parent, + __libSystem_atfork_prepare, + _clock_gettime_nsec_np, _pthread_create_from_mach_thread, _pthread_get_stackaddr_np, _pthread_get_stacksize_np, __pthread_self, dyld_stub_binder ] ... diff --git a/src/space.cpp b/src/space.cpp index 27da6d5..f9689b1 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -19,6 +19,25 @@ extern "C" { +// THE SYSTEM LIBRARY'S OWN PREPARATION FOR A DUPLICATE, WITHOUT WHICH THE +// DUPLICATE CANNOT START A THREAD. +// +// This system's `fork' is not only the primitive: around it the system library +// takes its own locks, and in the duplicate it makes the library's state true +// of the new process --- among the rest, the name of the task, which a +// duplicate receives anew and through which the thread library allocates every +// stack. The primitive alone left the duplicate holding the original's state, +// and the first thread it asked for failed: measured, `pthread_create' returned +// EAGAIN in a duplicate made here and 0 in the original (mcpplibs/openkal-musl#55). +// +// The three are the ones `fork' itself calls, under names no C library in this +// ecosystem defines, so they are reachable beneath one as above one. They are +// called in the order `fork' calls them: before the primitive, and then in each +// image the one that belongs to it. +void _libSystem_atfork_prepare(void); +void _libSystem_atfork_parent(void); +void _libSystem_atfork_child(void); + int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, kal_process* out) { if (entry == nullptr || out == nullptr) return kal_err_invalid; @@ -31,10 +50,12 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, (void)stack_top; bool is_duplicate = false; + _libSystem_atfork_prepare(); const okm_long child = okm::duplicate(is_duplicate); - if (okm::failed(child)) return okm::translate(child); + if (okm::failed(child)) { _libSystem_atfork_parent(); return okm::translate(child); } if (is_duplicate) { + _libSystem_atfork_child(); entry(arg); // THE ENTRY IS NOT REQUIRED TO RETURN, AND IF IT DOES THE CONTEXT ENDS. // @@ -46,6 +67,7 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, for (;;) okm::sys(okm::nr_exit, 0); } + _libSystem_atfork_parent(); *out = kal_process{ static_cast(child) }; return kal_ok; } diff --git a/tests/conformance_space_task.cpp b/tests/conformance_space_task.cpp new file mode 100644 index 0000000..1859df0 --- /dev/null +++ b/tests/conformance_space_task.cpp @@ -0,0 +1,64 @@ +// Conformance: a context started in a copy of the space can start a context. +// +// mcpplibs/openkal-musl#55: in a copy made by kal_space_start, the first +// thread the copy asked for failed with EAGAIN, while the original started one. +// The duplication primitive alone leaves the system library's state the +// original's, and this system's `fork' prepares it around the primitive; the +// implementation now does what `fork' does. Observed through the status, the +// only channel a separate space has: 3 is a context started, joined and given +// its least stack size in the copy, and any other value is the step that +// failed. +import openkal.types; +import openkal.task; +import openkal.space; +import openkal.process; +import openkal.stream; +import openkal.abort; + +namespace { +int failures = 0; +void check(bool ok, const char* what) { + if (ok) return; + ++failures; + const char pre[] = "FAIL: "; + kal::write(kal::err(), pre, sizeof(pre) - 1); + kal_uintptr n = 0; while (what[n]) ++n; + kal::write(kal::err(), what, n); + kal::write(kal::err(), "\n", 1); +} + +volatile int g_ran = 0; +void ran(void*) { g_ran = 1; } + +void in_the_copy(void*) { + kal_task t{}; + if (kal_task_start(ran, nullptr, &t) != kal_ok) kal_exit(4); + if (kal_task_join(t) != kal_ok || g_ran != 1) kal_exit(5); + kal_task s{}; + if (kal_task_start_sized(ran, nullptr, 16u << 20, &s) != kal_ok) kal_exit(6); + kal_task_join(s); + kal_exit(3); +} + +alignas(16) unsigned char g_stack[64 * 1024]; +} + +int main() { + for (int round = 0; round < 3; ++round) { + kal_process p{}; + const int rc = kal_space_start(in_the_copy, nullptr, g_stack + sizeof g_stack, &p); + check(rc == kal_ok, "a copy of the space starts"); + if (rc != kal_ok) break; + int status = 0, terminated = 0; + check(kal_process_wait(p, &status, &terminated) == kal_ok, "the copy is waited for"); + kal_process_close(p); + check(terminated == 0 && status == 3, "the copy started a context, joined it, and started one with a least stack size"); + if (status != 3) { + char line[] = "copy status: X\n"; + line[13] = static_cast('0' + (status % 10)); + kal::write(kal::err(), line, sizeof line - 1); + } + } + if (failures == 0) kal::write(kal::out(), "openkal-macos: a copy of the space starts contexts\n", 51); + return failures == 0 ? 0 : 1; +} From 9bac021ab95d7eaa8b0160fe16a1dec288fea761 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 08:40:46 +0900 Subject: [PATCH 2/4] The three preparations are left for the loader to find in a native link The system library exports them and its development stub does not list them, because they are not part of the interface it documents. -U leaves each for the loader to find among the loaded images; port/libSystem.tbd lists them for a link made elsewhere. --- mcpp.toml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mcpp.toml b/mcpp.toml index 2ee8e69..14773e8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -70,6 +70,21 @@ flags = [ "-fno-stack-protector", "-fno-builtin"] }, ] +# THREE NAMES THE SYSTEM LIBRARY EXPORTS AND ITS DEVELOPMENT STUB DOES NOT LIST. +# +# kal_space_start calls the preparations this system's `fork' performs around +# the duplication primitive (src/space.cpp, mcpplibs/openkal-musl#55). The +# library exports them --- `fork' lives in one of its parts and calls them in +# another --- and the stub a native link reads omits them, because they are not +# part of the interface the system documents. `-U' states that a name may be +# left for the loader to find among the images it has loaded, which is where +# it is. A link made elsewhere reads port/libSystem.tbd, which lists them, and +# is unaffected. The flags reach every program above this package: mcpp +# forwards a dependency's `ldflags' to its consumer's link. +ldflags = ["-Wl,-U,__libSystem_atfork_prepare", + "-Wl,-U,__libSystem_atfork_parent", + "-Wl,-U,__libSystem_atfork_child"] + [features] default = [] From d9139e0d6e4ebcccc1ffe5d6df20ba3363423963 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 08:56:45 +0900 Subject: [PATCH 3/4] The preparations for a duplicate are the parts fork calls, as measured _libSystem_atfork_* are not exported on macOS 14.8.9; the parts they call are. Measured in a program that duplicated itself with the primitive and asked the copy for a thread: nothing, EAGAIN; the two child preparations alone, the copy was killed; with the thread library's locks around the primitive, 0; the whole set in the library's order, five rounds of twenty threads, 0. The whole set is called, in that order, and named in port/libSystem.tbd and in -U flags. --- mcpp.toml | 36 ++++++++++++++-------- port/libSystem.tbd | 17 ++++++----- src/space.cpp | 74 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 89 insertions(+), 38 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 14773e8..751f1ce 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -70,20 +70,30 @@ flags = [ "-fno-stack-protector", "-fno-builtin"] }, ] -# THREE NAMES THE SYSTEM LIBRARY EXPORTS AND ITS DEVELOPMENT STUB DOES NOT LIST. +# THE PREPARATIONS FOR A DUPLICATE ARE LEFT FOR THE LOADER TO FIND. # -# kal_space_start calls the preparations this system's `fork' performs around -# the duplication primitive (src/space.cpp, mcpplibs/openkal-musl#55). The -# library exports them --- `fork' lives in one of its parts and calls them in -# another --- and the stub a native link reads omits them, because they are not -# part of the interface the system documents. `-U' states that a name may be -# left for the loader to find among the images it has loaded, which is where -# it is. A link made elsewhere reads port/libSystem.tbd, which lists them, and -# is unaffected. The flags reach every program above this package: mcpp -# forwards a dependency's `ldflags' to its consumer's link. -ldflags = ["-Wl,-U,__libSystem_atfork_prepare", - "-Wl,-U,__libSystem_atfork_parent", - "-Wl,-U,__libSystem_atfork_child"] +# kal_space_start calls the parts of the system library that this system's +# `fork' calls around the duplication primitive (src/space.cpp, +# mcpplibs/openkal-musl#55). They are exported, and they are not part of the +# interface the system documents, so a development stub need not list them. +# `-U' states that a name may be left for the loader to find among the images +# it has loaded, which is where it is; a name the stub does list is bound as +# usual. A link made elsewhere reads port/libSystem.tbd, which lists them. The +# flags reach every program above this package: mcpp forwards a dependency's +# `ldflags' to its consumer's link. +ldflags = ["-Wl,-U,__dyld_atfork_parent", + "-Wl,-U,__dyld_atfork_prepare", + "-Wl,-U,__dyld_fork_child", + "-Wl,-U,__libc_fork_child", + "-Wl,-U,__libc_fork_parent", + "-Wl,-U,__libc_fork_prepare", + "-Wl,-U,__mach_fork_child", + "-Wl,-U,__malloc_fork_child", + "-Wl,-U,__malloc_fork_parent", + "-Wl,-U,__malloc_fork_prepare", + "-Wl,-U,__pthread_atfork_child", + "-Wl,-U,__pthread_atfork_parent", + "-Wl,-U,__pthread_atfork_prepare"] [features] default = [] diff --git a/port/libSystem.tbd b/port/libSystem.tbd index d99dfc4..249936e 100644 --- a/port/libSystem.tbd +++ b/port/libSystem.tbd @@ -28,12 +28,12 @@ targets: [ arm64-macos, x86_64-macos ] install-name: '/usr/lib/libSystem.B.dylib' current-version: 1351.0.0 # -# EIGHT NAMES ARE CALLED AND ONE IS NOT. +# EIGHTEEN NAMES ARE CALLED AND ONE IS NOT. # -# The first five are names this implementation CALLS, and the enumeration above -# found them. The three `_libSystem_atfork_*' that 0.14.1 added are the -# preparation `fork' performs around the primitive, which a duplicate made by -# kal_space_start needs before it can start a thread (mcpplibs/openkal-musl#55). +# The first eighteen are names this implementation CALLS. The thirteen that +# 0.14.1 added are the preparations this system's `fork' performs around the +# primitive, part by part, which a duplicate made by kal_space_start needs +# before it can start a thread (src/space.cpp, mcpplibs/openkal-musl#55). # The three that 0.13.0 added answer the stack of the calling # thread (openkal 0.15, clause 11 entry 21), and they follow the same rule as # the two before them: this system's library defines them and no C library in @@ -57,8 +57,11 @@ current-version: 1351.0.0 # evidence about a format. exports: - targets: [ arm64-macos, x86_64-macos ] - symbols: [ __libSystem_atfork_child, __libSystem_atfork_parent, - __libSystem_atfork_prepare, + symbols: [ __dyld_atfork_parent, __dyld_atfork_prepare, __dyld_fork_child, + __libc_fork_child, __libc_fork_parent, __libc_fork_prepare, + __mach_fork_child, __malloc_fork_child, __malloc_fork_parent, + __malloc_fork_prepare, __pthread_atfork_child, __pthread_atfork_parent, + __pthread_atfork_prepare, _clock_gettime_nsec_np, _pthread_create_from_mach_thread, _pthread_get_stackaddr_np, _pthread_get_stacksize_np, __pthread_self, dyld_stub_binder ] diff --git a/src/space.cpp b/src/space.cpp index f9689b1..e734929 100644 --- a/src/space.cpp +++ b/src/space.cpp @@ -22,21 +22,59 @@ extern "C" { // THE SYSTEM LIBRARY'S OWN PREPARATION FOR A DUPLICATE, WITHOUT WHICH THE // DUPLICATE CANNOT START A THREAD. // -// This system's `fork' is not only the primitive: around it the system library -// takes its own locks, and in the duplicate it makes the library's state true -// of the new process --- among the rest, the name of the task, which a -// duplicate receives anew and through which the thread library allocates every -// stack. The primitive alone left the duplicate holding the original's state, -// and the first thread it asked for failed: measured, `pthread_create' returned -// EAGAIN in a duplicate made here and 0 in the original (mcpplibs/openkal-musl#55). +// This system's `fork' is not only the primitive. Around it each part of the +// system library takes its locks, and in the copy each makes its state true of +// the new process --- among the rest the name of the task, through which the +// thread library allocates every stack, and the thread library's own record of +// which threads exist. The primitive alone left the copy holding the original's +// state, and the first thread it asked for failed (mcpplibs/openkal-musl#55). // -// The three are the ones `fork' itself calls, under names no C library in this -// ecosystem defines, so they are reachable beneath one as above one. They are -// called in the order `fork' calls them: before the primitive, and then in each -// image the one that belongs to it. -void _libSystem_atfork_prepare(void); -void _libSystem_atfork_parent(void); -void _libSystem_atfork_child(void); +// MEASURED on macOS 14.8.9, in a program that duplicated itself with the +// primitive and asked the copy for a thread: +// +// nothing EAGAIN +// the two child preparations alone the copy was killed +// and the thread library's locks around 0 +// the whole set below, five times twenty 0 +// +// The library's own `fork' calls these from a routine it does not export, so +// the parts are called directly, in its order: locks before the primitive from +// the outermost part inward, released in the original in the reverse order, +// and the state renewed in the copy. Each is exported under a name no C library +// in this ecosystem defines, so it is reachable beneath one as above one. The +// handlers a program registers with `pthread_atfork' are not run: a copy made +// here is not a `fork' the program asked for. +void _dyld_atfork_prepare(void); +void _dyld_atfork_parent(void); +void _dyld_fork_child(void); +void _malloc_fork_prepare(void); +void _malloc_fork_parent(void); +void _malloc_fork_child(void); +void _libc_fork_prepare(void); +void _libc_fork_parent(void); +void _libc_fork_child(void); +void _pthread_atfork_prepare(void); +void _pthread_atfork_parent(void); +void _pthread_atfork_child(void); +void _mach_fork_child(void); + +} // extern "C" + +namespace { + +void prepare() { + _dyld_atfork_prepare(); _malloc_fork_prepare(); _libc_fork_prepare(); _pthread_atfork_prepare(); +} +void in_the_original() { + _pthread_atfork_parent(); _libc_fork_parent(); _malloc_fork_parent(); _dyld_atfork_parent(); +} +void in_the_copy() { + _mach_fork_child(); _dyld_fork_child(); _pthread_atfork_child(); _malloc_fork_child(); _libc_fork_child(); +} + +} // namespace + +extern "C" { int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, kal_process* out) { @@ -50,12 +88,12 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, (void)stack_top; bool is_duplicate = false; - _libSystem_atfork_prepare(); + prepare(); const okm_long child = okm::duplicate(is_duplicate); - if (okm::failed(child)) { _libSystem_atfork_parent(); return okm::translate(child); } + if (okm::failed(child)) { in_the_original(); return okm::translate(child); } if (is_duplicate) { - _libSystem_atfork_child(); + in_the_copy(); entry(arg); // THE ENTRY IS NOT REQUIRED TO RETURN, AND IF IT DOES THE CONTEXT ENDS. // @@ -67,7 +105,7 @@ int kal_space_start(void (*entry)(void*), void* arg, void* stack_top, for (;;) okm::sys(okm::nr_exit, 0); } - _libSystem_atfork_parent(); + in_the_original(); *out = kal_process{ static_cast(child) }; return kal_ok; } From b50f3de0cbfd51af4760b5ecb3f2e3266de004a9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 09:06:09 +0900 Subject: [PATCH 4/4] The independence check permits the thirteen preparations, and says why --- .github/workflows/ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5361c2..6cf5ddd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -329,6 +329,15 @@ jobs: # first is deliberate: `pthread_self' IS a name musl defines, and # this system's library would be handed a thread record of # another library's layout. + # _dyld_atfork_*, _dyld_fork_child, + # _malloc_fork_*, _libc_fork_*, + # _pthread_atfork_*, _mach_fork_child the preparations this + # system's `fork' performs around the duplication primitive, part + # by part, which kal_space_start calls so that a copy can start a + # thread (src/space.cpp, mcpplibs/openkal-musl#55). The routine + # `fork' calls them from is not exported; each part is, under a + # name no C library in this ecosystem defines. None of them calls + # back into a C library above. # __libc_start_main, main, _main the hand-over, undefined here by # construction. # kal_* the interface itself. @@ -348,7 +357,7 @@ jobs: # ___dso_handle likewise the format's: emitted # beside the weak references above, defined by the image's own # start files. - permitted='^_?(memcpy|memmove|memset|memcmp|bzero|clock_gettime_nsec_np|pthread_create_from_mach_thread|pthread_create|pthread_join|_pthread_self|_pthread_get_stackaddr_np|_pthread_get_stacksize_np|__libc_start_main|main|kal_[a-z_]+|__stack_chk_guard|__stack_chk_fail|GCC_except_table.*|_ZN3okm.*|__Unwind_Resume|__dso_handle|section\$(start|end)\$__TEXT\$__(eh_frame|unwind_info))$' + permitted='^_?(memcpy|memmove|memset|memcmp|bzero|clock_gettime_nsec_np|pthread_create_from_mach_thread|pthread_create|pthread_join|_pthread_self|_pthread_get_stackaddr_np|_pthread_get_stacksize_np|_dyld_atfork_(prepare|parent)|_dyld_fork_child|_(malloc|libc)_fork_(prepare|parent|child)|_pthread_atfork_(prepare|parent|child)|_mach_fork_child|__libc_start_main|main|kal_[a-z_]+|__stack_chk_guard|__stack_chk_fail|GCC_except_table.*|_ZN3okm.*|__Unwind_Resume|__dso_handle|section\$(start|end)\$__TEXT\$__(eh_frame|unwind_info))$' # Reported with the object that references it. A symbol without the # object it came from names a fault and not a place, and the first # time this check fired the answer was in the object rather than in