Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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
Expand Down
108 changes: 107 additions & 1 deletion src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<kal_uintptr>(first) }; // the reading end
*theirs = kal_stream{ static_cast<kal_uintptr>(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<okm_long>(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<okm_long>(streams->in) : 0;
const okm_long ou = streams ? static_cast<okm_long>(streams->out) : 0;
const okm_long er = streams ? static_cast<okm_long>(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<okm_long>(3 + i));

okm::sys(nr_fchdir, b);
okm::sys(okm::nr_execve, reinterpret_cast<okm_long>(p.buf),
reinterpret_cast<okm_long>(args.slots),
reinterpret_cast<okm_long>(envs.slots));
for (;;) okm::sys(okm::nr_exit, 127);
}

*out = kal_process{ static_cast<kal_uintptr>(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;
Expand Down Expand Up @@ -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;

}
34 changes: 33 additions & 1 deletion src/sys.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,46 @@ 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.
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,
Expand Down
129 changes: 129 additions & 0 deletions src/terminal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#include "sys.h"
#include <openkal/terminal.h>

// 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<okm_long>(s.h),
tiocgeta, reinterpret_cast<okm_long>(&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<okm_long>(s.h),
tiocseta, reinterpret_cast<okm_long>(&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<okm_long>(s.h),
tiocgwinsz, reinterpret_cast<okm_long>(&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<kal_uintptr>(w.col);
*rows = static_cast<kal_uintptr>(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<okm_long>(s.h),
tiocgwinsz, reinterpret_cast<okm_long>(&w));
if (!okm::failed(r)) p |= KAL_TERM_PROP_SIZE;

return p;
}

} // extern "C"
Loading