From f1c3be8d8659d668e5ff748d48505cf0027be5c5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 31 Aug 2026 10:33:43 +0800 Subject: [PATCH] =?UTF-8?q?0.12.0=20=E2=80=94=20a=20program=20that=20needs?= =?UTF-8?q?=20an=20interpreter=20could=20not=20be=20started=20at=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️⚠️ A defect this implementation has shipped, on every architecture, for as long as `kal_process_spawn` has used `execveat`. `execveat` with a directory descriptor and a relative name gives the kernel the program's name as `/dev/fd//`. For an ordinary executable that spelling never surfaces — the kernel holds the file open already. For a program that needs an INTERPRETER it does: the kernel starts the interpreter and hands it that name TO OPEN, after the replacement, by which time a close-on-exec descriptor is gone. The interpreter is told the file does not exist. Two kinds of program need one, and both were refused: a `#!' script on every architecture a binary of another architecture through binfmt_misc ⭐ Isolated in twenty lines of ordinary C with nothing of openkal in it: dirfd WITH O_CLOEXEC execveat -> ENOENT dirfd WITHOUT O_CLOEXEC STARTED ok The base descriptor is now duplicated in the STARTED image, where clearing close-on-exec costs the caller nothing: its own descriptors stay as they were, which is what every other operation here relies upon. ⚠️⚠️ HOW IT WAS NEARLY MISSED, which is worth more than the fix. It surfaced on aarch64 under emulation, where every foreign binary needs the binfmt interpreter and so every start failed at once — and three true observations pointed the wrong way: the previous release failed identically, a consumer's 108 tests passed on both architectures, and native binaries started fine in the same run. Each was true; "therefore it is the emulator" was not. Reproducing it natively with a `#!' script took one probe. --- mcpp.toml | 2 +- src/process.cpp | 33 ++++++++++++++++++++++++++++++++- src/sys.h | 7 +++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index 95f036c..e3c8db6 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.11.0" +version = "0.12.0" description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" diff --git a/src/process.cpp b/src/process.cpp index 66a70ae..33434e8 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -279,8 +279,39 @@ int kal_process_spawn(const kal_spawn* how, okl::sys(okl::nr_exit_group, 127); } + // ⚠️⚠️ THE BASE IS DUPLICATED SO THAT IT SURVIVES THE REPLACEMENT, AND + // WITHOUT THIS A WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED AT ALL. + // + // `execveat' with a dirfd and a relative name gives the program's name to + // the kernel as `/dev/fd//'. That spelling is invisible to a + // caller and harmless for an ordinary executable --- the kernel already + // holds the file open. It stops being harmless the moment the program + // needs an INTERPRETER: a `#!' script, or a binary of another + // architecture registered through `binfmt_misc'. The kernel then starts + // the interpreter and hands it that name to open --- AFTER the + // replacement, by which time a close-on-exec dirfd is gone. The + // interpreter is told the script does not exist. + // + // ⭐ Measured in twenty lines of plain C, with everything else identical: + // + // dirfd WITH O_CLOEXEC execveat -> ENOENT + // dirfd WITHOUT O_CLOEXEC STARTED ok + // + // ⚠️ It is not a property of one architecture. It was FOUND on aarch64, + // where every foreign binary needs the binfmt interpreter and so every + // start failed --- and it was mistaken there for a limit of the emulator. + // It reproduces natively on x86_64 with a `#!' script, which is what a + // consumer meets on any machine. + // + // ⚠️ Duplicated HERE, in the started image, and not where the preopens are + // made: the caller's own descriptors stay close-on-exec, which is what + // every other operation of this implementation relies upon. `dup' clears + // the flag by definition, so the copy is the exec-visible one. + const okl_long visible = okl::sys(okl::nr_fcntl, b, okl::f_dupfd, 0); + const okl_long base = okl::failed(visible) ? b : visible; + const okl_long why = - okl::sys(okl::nr_execveat, b, reinterpret_cast(p.buf), + okl::sys(okl::nr_execveat, base, reinterpret_cast(p.buf), reinterpret_cast(args.slots), reinterpret_cast(envs.slots), 0); // Reached only when the replacement did not happen, because when it does diff --git a/src/sys.h b/src/sys.h index 14a3ec5..82c4078 100644 --- a/src/sys.h +++ b/src/sys.h @@ -258,6 +258,13 @@ enum : okl_long { // closes what is on it. f_dupfd_cloexec = 1030, + // ⭐ The same primitive WITHOUT the flag, which is the point of having both. + // A descriptor duplicated this way survives a replacement, and starting a + // program that needs an interpreter depends on exactly that --- see the + // duplication in `kal_process_spawn'. `dup' would do as well and this + // architecture pair does not agree on whether it exists. + f_dupfd = 0, + // ⭐⭐ THE OPEN-FILE FORM AND NOT THE PROCESS FORM, WHICH IS THE WHOLE // DIFFERENCE. //