diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f6afd0..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' @@ -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. diff --git a/mcpp.toml b/mcpp.toml index 2d02cb4..38d705a 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 = "0.8.0" [build] # The flags are attached to this package's own sources rather than to the whole 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..ffee25f 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,7 @@ 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_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, 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"