From 354bab953c4197c8898fc8092e92c8a6288b07b9 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 10:13:50 +0800 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20=E6=8A=8A=E5=BC=95=E5=AF=BC=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E7=BB=91=E5=AE=9A=E6=8C=87=E5=90=8D=E7=9A=84=20glibc?= =?UTF-8?q?=20=E8=BD=BD=E8=8D=B7=E8=A3=85=E7=BB=99=E5=AE=83(=E8=BF=87?= =?UTF-8?q?=E6=B8=A1=E6=AD=A5=E9=AA=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xim:glibc` 的 `latest` 从 `2.44` 移到了 `2.44.2`。载荷目录按请求**解析成**的版本 命名,而 RuntimeBinding 带的是**声明的**版本 —— 而一个已发布的 mcpp vendor 进自己 沙箱的那份 xlings 仍然声明 `2.44`。于是干净机器装出 `2.44.2`,工具链 fixup 要 `2.44`,构建在编译任何东西之前就停住: error: selected RuntimeBinding glibc@2.44 requires payload '.../xpkgs/xim-x-glibc/2.44', but it is not installed ⚠️ 在每一台**新**机器上出现,在任何已存在的机器上都不出现 —— 所以开发机看不见。 索引的 `pkgs/g/glibc.lua` 把这次失败逐字记着,并给出规则: 「The index is DATA and the client is a PROGRAM: the consumer ships first。」 ## ⚠️ 我先试过 bump xlings,撤回了 把 `XLINGS_VERSION` 提到 2026.8.27.1 会让**沙箱里那份** xlings 被换掉 (mcpp 的 `acquire_xlings_binary` 在 pin 前移时更新它),绑定于是**前移**到 `glibc@2.44.2`,而已经装好的是 `2.44` —— 请求比已装的**更新**。那是更糟的一侧: 接受一个更旧的载荷去回答更新的请求,等于静默用上索引明说 「IT IS NOT THE ONE TO INSTALL」的那份(带 mcpp#484 的 preload 缺陷)。 ⇒ 保持 xlings 不动,把缺的那个载荷直接装上。 ⭐ 待 mcpp 2026.8.27.1(mcpp-community/mcpp#515)发布后移除:它接受 「版本精化了请求」的已装载荷,从它引导就不需要这一步。 --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f6afd0..2ca2fbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,34 @@ jobs: else xlings install "mcpp@$MCPP_VERSION" -y -g fi + # ⚠️⚠️ TRANSITION: GIVE THE BOOTSTRAP THE glibc ITS BINDING NAMES. + # + # `xim:glibc`'s `latest` moved from `2.44` to `2.44.2`. A payload + # directory is named after the version a request RESOLVED to, while a + # RuntimeBinding carries the version that was DECLARED — and the xlings + # a released mcpp vendors into its own sandbox still declares `2.44`. + # So a clean machine installs `2.44.2`, the toolchain fixup asks for + # `2.44`, and the build stops before anything is compiled: + # + # error: selected RuntimeBinding glibc@2.44 requires payload + # '…/xpkgs/xim-x-glibc/2.44', but it is not installed + # + # ⚠️ On every NEW machine and on none that already existed, which is why + # it is invisible from a developer's own. Measured on `main` as readily + # as on any branch — the index records the same failure verbatim in + # `pkgs/g/glibc.lua` and states the rule it broke: "The index is DATA + # and the client is a PROGRAM: the consumer ships first." + # + # ⭐ REMOVE THIS once a released mcpp resolves it. `mcpp 2026.8.27.1` + # accepts an installed payload whose version REFINES the requested one + # (`payload_dir_for_version`), so a bootstrap from it needs nothing + # here. Until then the missing payload is simply installed. + if [ -x "$HOME/.mcpp/registry/bin/xlings" ]; then + XLINGS_HOME="$HOME/.mcpp/registry" XLINGS_NON_INTERACTIVE=1 \ + "$HOME/.mcpp/registry/bin/xlings" install glibc@2.44 -y -g \ + >/dev/null 2>&1 || true + echo "glibc payloads present: $(ls "$HOME/.mcpp/registry/data/xpkgs/xim-x-glibc" 2>/dev/null | tr '\n' ' ')" + fi mcpp --version mcpp self config --mirror GLOBAL # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. From a00e32a65b9f8cebcc43ca9bc558e4600a25fe1a Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 13:28:23 +0800 Subject: [PATCH 2/5] Implement openkal.terminal, and follow openkal 0.8 The terminal ioctls this kernel offers are the ones kal_stream_props already uses, so the interface is a natural extension rather than a new mechanism: TIOCGETA and TIOCSETA for the mode, TIOCGWINSZ for the size. The request numbers encode the size of the structure they carry, so the layout is declared here and asserted rather than named from a header. A header stating it would belong to a C library, and this implementation has none. The mode is read, modified and written rather than written alone. The structure carries a baud rate and twenty control characters that the interface does not name; composing one from the mode word would discard them, and the terminal a program returned to would not be the one it found. kal_terminal_props asks TIOCGWINSZ rather than deriving the answer from TIOCGETA. A pseudo terminal answers both and a serial line answers only the first, so a word derived from one would claim a facility the next call refuses. The other four interfaces version 0.8 adds are not provided, which clause 6.1 makes an absence at the link rather than a deviation. --- mcpp.toml | 4 +- src/terminal.cpp | 129 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 src/terminal.cpp diff --git a/mcpp.toml b/mcpp.toml index 2d02cb4..9b7e7ff 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.3.4" +version = "0.4.0" 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" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-macos" [dependencies] -openkal = "0.7.0" +openkal = { git = "https://github.com/mcpplibs/openkal", branch = "feat/openkal-0.8" } [build] # The flags are attached to this package's own sources rather than to the whole diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..f0f8b25 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,129 @@ +#include "sys.h" +#include + +// openkal.terminal upon this kernel's terminal ioctls. +// +// THE REQUESTS ENCODE THE SIZE OF THE STRUCTURE THEY CARRY, which is why they +// are written out here rather than named from a header: the number is a property +// of this kernel's layout, and a header that stated it would belong to a C +// library this implementation does not have. kal_stream_props already spells +// TIOCGETA the same way and for the same reason. + +namespace { + +// This kernel's terminal settings, in this kernel's layout. Four flag words of a +// machine word each, twenty control characters, and two speeds. The size is +// seventy-two bytes, which is the number the requests below carry. +struct oktermios { + okm_ulong iflag; + okm_ulong oflag; + okm_ulong cflag; + okm_ulong lflag; + unsigned char cc[20]; + okm_ulong ispeed; + okm_ulong ospeed; +}; +static_assert(sizeof(oktermios) == 72, + "the request numbers below carry this size"); + +struct okwinsize { + unsigned short row; + unsigned short col; + unsigned short xpixel; + unsigned short ypixel; +}; +static_assert(sizeof(okwinsize) == 8, "the request number below carries this size"); + +// _IOR('t', 19, struct termios) and _IOW('t', 20, struct termios), and +// _IOR('t', 104, struct winsize). +constexpr okm_long tiocgeta = 0x40000000L | (72L << 16) | ('t' << 8) | 19; +constexpr okm_long tiocseta = 0x80000000L | (72L << 16) | ('t' << 8) | 20; +constexpr okm_long tiocgwinsz = 0x40000000L | (8L << 16) | ('t' << 8) | 104; + +// Positions within lflag. This kernel's values, which are not the other's. +constexpr okm_ulong t_echo = 0x00000008u; +constexpr okm_ulong t_icanon = 0x00000100u; + +kal_uintptr mode_of(const oktermios& t) { + kal_uintptr m = 0; + if ((t.lflag & t_icanon) != 0) m |= KAL_TERM_LINE_EDIT; + if ((t.lflag & t_echo) != 0) m |= KAL_TERM_ECHO; + return m; +} + +int get_termios(kal_stream s, oktermios& out) { + const okm_long r = okm::sys(okm::nr_ioctl, static_cast(s.h), + tiocgeta, reinterpret_cast(&out)); + // A stream that is not a terminal is reported as unsupported rather than + // having this kernel's own classification passed through. + if (okm::failed(r)) return kal_err_not_supported; + return kal_ok; +} + +} // namespace + +extern "C" { + +int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { + if (mode == nullptr) return kal_err_invalid; + oktermios t{}; + const int rc = get_termios(s, t); + if (rc != kal_ok) return rc; + *mode = mode_of(t); + return kal_ok; +} + +int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { + // READ, MODIFY, WRITE. The structure carries a baud rate and twenty control + // characters that this interface does not name; composing one from the mode + // word alone would discard them, and the terminal a program returned to + // would not be the one it found. + oktermios t{}; + const int rc = get_termios(s, t); + if (rc != kal_ok) return rc; + + if ((mode & KAL_TERM_LINE_EDIT) != 0) t.lflag |= t_icanon; + else t.lflag &= ~t_icanon; + if ((mode & KAL_TERM_ECHO) != 0) t.lflag |= t_echo; + else t.lflag &= ~t_echo; + + // A position this implementation does not distinguish is ignored rather than + // refused, which clause 6.2 requires: a program compiled against a later + // revision sets a position this build has never heard of. + const okm_long w = okm::sys(okm::nr_ioctl, static_cast(s.h), + tiocseta, reinterpret_cast(&t)); + if (okm::failed(w)) return kal_err_not_supported; + return kal_ok; +} + +int kal_terminal_size(kal_stream s, kal_uintptr* cols, kal_uintptr* rows) { + if (cols == nullptr || rows == nullptr) return kal_err_invalid; + okwinsize w{}; + const okm_long r = okm::sys(okm::nr_ioctl, static_cast(s.h), + tiocgwinsz, reinterpret_cast(&w)); + // Both outputs are left untouched, which the interface requires: a serial + // line answers TIOCGETA and not this, so a caller must be able to tell the + // two conditions apart. + if (okm::failed(r)) return kal_err_not_supported; + *cols = static_cast(w.col); + *rows = static_cast(w.row); + return kal_ok; +} + +kal_uintptr kal_terminal_props(kal_stream s) { + kal_uintptr p = 0; + + oktermios t{}; + if (get_termios(s, t) == kal_ok) p |= KAL_TERM_PROP_MODE; + + // The size is asked for rather than derived from the first. Deriving it + // would make the word claim a facility the very next call refuses. + okwinsize w{}; + const okm_long r = okm::sys(okm::nr_ioctl, static_cast(s.h), + tiocgwinsz, reinterpret_cast(&w)); + if (!okm::failed(r)) p |= KAL_TERM_PROP_SIZE; + + return p; +} + +} // extern "C" From 3691fdadfac071c4c09fcc3598f4205ca86d72ec Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 14:00:52 +0800 Subject: [PATCH 3/5] Provide the three operations openkal 0.8 adds to openkal.process ADDING TO AN EXISTING INTERFACE OBLIGES EVERY IMPLEMENTATION OF IT, and adding a new interface obliges none. Clause 6.1 makes an interface a backend does not provide absent at the link and not a deviation; it makes one provided IN PART a deviation. The five interfaces version 0.8 adds are therefore free to decline, and the three names added to openkal.process are not. The specification's own surface checker said so before anything else noticed: openkal.process is provided in part: 3 of 8 names are not exported -- kal_process_channel kal_process_channel_close kal_process_spawn_with kal_process_channel is this kernel's `pipe', which reports BOTH descriptors as return values rather than through a buffer. That is a property of the calling convention, and src/sys.h already meets the same difference for the duplication primitive; a second wrapper is added beside it rather than the difference being hidden. There is no pipe2 here, so close-on-exec is set afterwards with fcntl. The two steps are not equivalent to one under a concurrent spawn, and that is stated rather than concealed: a caller that spawns from one context, which is what a program using this operation does, is unaffected. dup2 onto the same number succeeds and does nothing, where the other kernel's dup3 refuses. Either is right for the placement loop; only the reason differs, and it is written down so that a reader comparing the two implementations does not take one for an oversight. --- src/process.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++++++- src/sys.h | 35 +++++++++++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/src/process.cpp b/src/process.cpp index 39a5c66..54716a9 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -108,6 +108,111 @@ int kal_process_spawn(kal_dir base, return kal_ok; } +// A channel: a pair of streams of which one end is meant to cross a spawn. +// +// This kernel's `pipe' reports BOTH descriptors as return values rather than +// through a buffer, which is a property of its calling convention and not of the +// call: the second value comes back in the second register. src/sys.h says the +// same thing about the duplication primitive, and for the same reason. +// +// THERE IS NO pipe2 HERE, so close-on-exec is set afterwards with fcntl. Doing +// it in two steps is not equivalent under a concurrent spawn --- another context +// starting a program between the two would inherit the descriptors --- and this +// implementation states that rather than concealing it. A caller that spawns +// from one context, which is what a program using this operation does, is not +// affected. +int kal_process_channel(kal_stream* mine, kal_stream* theirs) { + if (mine == nullptr || theirs == nullptr) return kal_err_invalid; + + okm_long second = 0; + const okm_long first = okm::pipe_pair(second); + if (okm::failed(first)) return okm::translate(first); + + constexpr okm_long f_setfd = 2, fd_cloexec = 1; + okm::sys(okm::nr_fcntl, first, f_setfd, fd_cloexec); + okm::sys(okm::nr_fcntl, second, f_setfd, fd_cloexec); + + // Bare descriptors, because openkal.stream's transfer operations take what + // this kernel takes. kal_fs_stream reports a file's stream the same way. + *mine = kal_stream{ static_cast(first) }; // the reading end + *theirs = kal_stream{ static_cast(second) }; // the writing end + return kal_ok; +} + +void kal_process_channel_close(kal_stream s) { + // The standard streams are borrowed and are numbered 0, 1 and 2; closing one + // of those through this operation would take a stream away from the whole + // program. + const okm_long fd = static_cast(s.h); + if (fd < 3) return; + okm::sys(okm::nr_close, fd); +} + +// Starting a program that receives exactly the directories named. +// +// The grants are placed as descriptors three and upward, which is where +// kal_fs_preopen reads them back from. The inverse relationship clause 7.11 +// describes is between those two operations, which is why they must agree about +// the numbering rather than each choosing one. +int kal_process_spawn_with(kal_dir base, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const kal_spawn_streams* streams, + const kal_preopen* grants, kal_uintptr grant_count, + kal_process* out) { + const int b = okm::unpack(base.h); + if (b < 0 || out == nullptr) return kal_err_invalid; + if (!okm::acceptable(path, path_len)) return kal_err_invalid; + if (grant_count > 0 && grants == nullptr) return kal_err_invalid; + okm::terminated p(path, path_len); + if (!p.ok) return kal_err_invalid; + + vector args, envs; + if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; + if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; + + // Resolved before the duplication, because a failure after it would leave a + // child to be reaped and a caller holding an error it cannot act upon. + constexpr kal_uintptr max_grants = 16; + if (grant_count > max_grants) return kal_err_invalid; + int granted[max_grants]; + for (kal_uintptr i = 0; i < grant_count; ++i) { + granted[i] = okm::unpack(grants[i].dir.h); + if (granted[i] < 0) return kal_err_invalid; + } + + const okm_long in = streams ? static_cast(streams->in) : 0; + const okm_long ou = streams ? static_cast(streams->out) : 0; + const okm_long er = streams ? static_cast(streams->err) : 0; + + bool is_duplicate = false; + const okm_long child = okm::duplicate(is_duplicate); + if (okm::failed(child)) return okm::translate(child); + + if (is_duplicate) { + if (in != 0) okm::sys(okm::nr_dup2, in, 0); + if (ou != 0) okm::sys(okm::nr_dup2, ou, 1); + if (er != 0) okm::sys(okm::nr_dup2, er, 2); + + // dup2 onto the same number succeeds and does nothing, unlike dup3, + // which refuses. Either behaviour is right for this loop; only the + // reason differs, and it is stated so that a reader comparing the two + // implementations does not take one of them for an oversight. + for (kal_uintptr i = 0; i < grant_count; ++i) + okm::sys(okm::nr_dup2, granted[i], static_cast(3 + i)); + + okm::sys(nr_fchdir, b); + okm::sys(okm::nr_execve, reinterpret_cast(p.buf), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots)); + for (;;) okm::sys(okm::nr_exit, 127); + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) { if (h.h == 0) return kal_err_invalid; int st = 0; @@ -144,6 +249,7 @@ void kal_process_close(kal_process) { } const kal_uintptr kal_process_props = KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING - | KAL_PROCESS_PROP_EXIT_STATUS; + | KAL_PROCESS_PROP_EXIT_STATUS + | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; } diff --git a/src/sys.h b/src/sys.h index da94894..bb76e4c 100644 --- a/src/sys.h +++ b/src/sys.h @@ -122,6 +122,38 @@ inline okm_long duplicate(bool& is_duplicate) { #endif } +// Creating a pipe, which is the second call whose result does not fit the +// convention above and does so for the same reason as the first. +// +// This kernel reports BOTH descriptors as return values: the reading end in the +// first register and the writing end in the second. The other kernel takes a +// buffer and fills it. Neither is more natural; what matters is that this file +// meets the difference rather than hiding it, as it does for the duplication +// primitive above. +inline okm_long pipe_pair(okm_long& writing) { +#if defined(__aarch64__) + register okm_long x16 __asm__("x16") = 42; // pipe + register okm_long x0 __asm__("x0") = 0; + register okm_long x1 __asm__("x1") = 0; + okm_long failed; + __asm__ __volatile__("svc #0x80\n\tcset %2, cs" + : "+r"(x0), "+r"(x1), "=r"(failed) + : "r"(x16) + : "memory", "cc"); + writing = x1; + return failed ? -x0 : x0; +#else + okm_long first, second; + unsigned char failed; + __asm__ __volatile__("syscall" + : "=a"(first), "=d"(second), "=@ccc"(failed) + : "a"(42L | 0x2000000L) + : "rcx", "r11", "memory", "cc"); + writing = second; + return failed ? -first : first; +#endif +} + // The numbers. They are the same on both architectures this implementation // supports, which is the reason the table is not per-architecture as it is on // the other kernel. @@ -129,7 +161,8 @@ enum : okm_long { nr_exit = 1, nr_read = 3, nr_write = 4, nr_close = 6, nr_wait4 = 7, nr_chdir = 12, nr_getpid = 20, nr_getuid = 24, nr_geteuid = 25, nr_kill = 37, nr_dup = 41, nr_getegid = 43, nr_getgid = 47, - nr_ioctl = 54, nr_execve = 59, nr_umask = 60, + nr_ioctl = 54, nr_execve = 59, nr_umask = 60, nr_pipe = 42, + nr_fcntl = 92, nr_munmap = 73, nr_mprotect = 74, nr_madvise = 75, nr_dup2 = 90, nr_fsync = 95, nr_gettimeofday = 116, nr_readv = 120, nr_writev = 121, nr_ftruncate = 201, From 8309558ec122c195133840ed43e665b66b0a7543 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 14:08:26 +0800 Subject: [PATCH 4/5] Do not redeclare nr_fcntl The number was already in the table. I added it again beside nr_pipe rather than looking, and this kernel's table is one enumeration, so the second declaration is an error rather than a duplicate: src/sys.h:178:5: error: redefinition of enumerator 'nr_fcntl' Not visible from a machine that cannot build for this system, which is why the check that found it is the one that runs on it. The whole table was then examined for the same shape rather than this one line corrected: no other name is declared twice, in either architecture's block, in this repository or in the other backend that has such a table. --- src/sys.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sys.h b/src/sys.h index bb76e4c..ffee25f 100644 --- a/src/sys.h +++ b/src/sys.h @@ -162,7 +162,6 @@ enum : okm_long { nr_chdir = 12, nr_getpid = 20, nr_getuid = 24, nr_geteuid = 25, nr_kill = 37, nr_dup = 41, nr_getegid = 43, nr_getgid = 47, nr_ioctl = 54, nr_execve = 59, nr_umask = 60, nr_pipe = 42, - nr_fcntl = 92, nr_munmap = 73, nr_mprotect = 74, nr_madvise = 75, nr_dup2 = 90, nr_fsync = 95, nr_gettimeofday = 116, nr_readv = 120, nr_writev = 121, nr_ftruncate = 201, From bf01d92443c0f5969ce9e85719308b1e87912c8d Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 17:35:55 +0800 Subject: [PATCH 5/5] Name the released specification and the released engine openkal 0.8.0 is published, so the manifest names it by version rather than by branch. A branch is how a change spanning these repositories is developed and is not a form a published package may carry: a consumer resolving from the index would be handed a reference that moves. The engine pin moves to mcpp 2026.8.27.1 and the repository variable MCPP_SOURCE_REF is cleared, so what this repository tests is the engine a user installs. --- .github/workflows/ci.yml | 2 +- mcpp.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ca2fbf..32a25a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ on: default: "" env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF }} - MCPP_VERSION: 2026.8.26.2 + MCPP_VERSION: 2026.8.27.1 XLINGS_VERSION: v2026.8.17.2 XLINGS_NON_INTERACTIVE: '1' diff --git a/mcpp.toml b/mcpp.toml index 9b7e7ff..38d705a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-macos" [dependencies] -openkal = { git = "https://github.com/mcpplibs/openkal", branch = "feat/openkal-0.8" } +openkal = "0.8.0" [build] # The flags are attached to this package's own sources rather than to the whole