From 24bbb5056a67149bf1abbf619a04af0caafd0d33 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 30 Oct 2026 01:23:37 +0900 Subject: [PATCH] 0.18.3 --- a directory handle names a directory and does not read one Every kal_dir, the two preopens included, is opened with O_PATH: resolving beneath it, starting a program in it and describing it read nothing, and listing already opens a readable descriptor of its own. Measured on Android 14 under run-as (SELinux runas_app, mcpp#857): the preopen of / failed with EACCES, and every program started by absolute name was reported as not found. The comment on the ENOSYS fallback now states what the code does: each start that meets ENOSYS falls back in the started image, and no start learns it from another. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 2 +- mcpp.toml | 2 +- src/fs.cpp | 27 +++++++++--- src/process.cpp | 6 ++- tests/search_only_base.cpp | 89 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 tests/search_only_base.cpp diff --git a/README.md b/README.md index 4802346..910b5dc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface. openkal = "0.17.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.18.2" +openkal-linux = "0.18.3" ``` ## Why it does not use a C library diff --git a/mcpp.toml b/mcpp.toml index b1a35eb..23f7b6b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.18.2" +version = "0.18.3" 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/fs.cpp b/src/fs.cpp index a07d36a..9ff7b6a 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -82,6 +82,25 @@ bool granted(preopen* t, kal_uintptr* n) { return true; } +// A DIRECTORY HANDLE NAMES A DIRECTORY; IT DOES NOT READ ONE (0.18.3). +// +// Every `kal_dir' is a base: names are resolved beneath it (`*at'), a program +// is started in it (`fchdir'), and it is described (`fstat', `fstatfs'). None +// of these reads the directory, and `O_PATH' is the open that asks for no +// access but naming. Reading happens only where entries are listed, and an +// enumeration already opens a descriptor of its own through the handle, with +// read access, at that moment. +// +// Until 0.18.2 the handles were opened for reading, and a directory the +// program may search but not read could not be a base at all. Measured on +// Android 14 under `run-as' (SELinux domain `runas_app', mcpp#857): the +// preopen of `/' failed with EACCES, so every name resolved beneath the root, +// and every program started by absolute name, failed before anything was +// started --- reported as "not found", because the base it would have been +// found in was never opened. The same program, its directory and its exec +// path all worked when each step was called directly. +constexpr okl_long dir_handle_flags = okl::o_path | okl::o_directory | okl::o_cloexec; + preopen* table(kal_uintptr* count) { static preopen t[kMaxGrants]; static kal_uintptr n = 2; @@ -102,11 +121,9 @@ preopen* table(kal_uintptr* count) { else { g_cwd[0] = '.'; g_cwd[1] = '\0'; cwd_len = 1; } const okl_long fd0 = okl::sys(okl::nr_openat, okl::at_fdcwd, - reinterpret_cast("."), - okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + reinterpret_cast("."), dir_handle_flags, 0); const okl_long fd1 = okl::sys(okl::nr_openat, okl::at_fdcwd, - reinterpret_cast("/"), - okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + reinterpret_cast("/"), dir_handle_flags, 0); t[0] = { g_cwd, cwd_len, okl::failed(fd0) ? 0u : okl::pack(static_cast(fd0)) }; t[1] = { "/", 1, okl::failed(fd1) ? 0u : okl::pack(static_cast(fd1)) }; } @@ -398,7 +415,7 @@ int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* ou if (b < 0 || out == nullptr || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), - okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + dir_handle_flags, 0); if (okl::failed(fd)) return okl::translate(fd); *out = kal_dir{ okl::pack(static_cast(fd)) }; return kal_ok; diff --git a/src/process.cpp b/src/process.cpp index ac2cb80..5218427 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -344,7 +344,11 @@ inline okl_long handle_pid(kal_process h) { return static_cast(h.h & 0 // the duplicate still allocates nothing. It costs the property `execveat' // bought, that the name is resolved through the descriptor, and is paid only // where `execveat' is unusable: from the first start under a translator this -// implementation recognises, and after the first ENOSYS anywhere else. +// implementation recognises, and, elsewhere, by each start that meets ENOSYS, +// which falls back in the started image itself. That fallback is learned by +// no other start: the image is a copy made by `clone' without shared memory, +// so nothing it learns reaches this program, and the price is one refused +// system call per start. // // Ported from the copy mcppls carried (vendor/openkal-linux 0.15.1), which set // the flag itself under PRoot; recognising the translator here removes that. diff --git a/tests/search_only_base.cpp b/tests/search_only_base.cpp new file mode 100644 index 0000000..6063cdd --- /dev/null +++ b/tests/search_only_base.cpp @@ -0,0 +1,89 @@ +// A directory the program may search but not read is still a base (0.18.3). +// +// A `kal_dir' names a directory; resolving beneath it, starting a program in +// it and describing it read nothing, so none of them needs the right to read +// it. Only listing does. Until 0.18.2 every handle was opened for reading, and +// on Android 14 under `run-as' (SELinux `runas_app') the root preopen itself +// failed with EACCES: every program started by absolute name was reported as +// not found (mcpp#857). +// +// Here the same situation is made with a mode instead of a policy: a directory +// of mode 0111. A privileged user reads it regardless, so the one assertion +// that it cannot be listed is made only for an unprivileged one. +#include +#include +#include +#include +import openkal.fs; +import openkal.stream; +import openkal.types; +import openkal.process; + +namespace { +int failures = 0; +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +kal_dir root() { + for (kal_uintptr i = 0; i < kal::fs::preopen_count(); ++i) { + kal_dir d{}; char name[512]; kal_uintptr len = 0; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) == kal_ok && len == 1 && name[0] == '/') + return d; + } + return kal_dir{}; +} +} + +int main() { + const auto work = kal::fs::working(); + check(kal_fs_mkdir(work, "okl_search_only", 15) == kal_ok, "a directory is created"); + kal_dir d{}; + check(kal_fs_open_dir(work, "okl_search_only", 15, &d) == kal_ok, "it opens"); + kal_file f{}; + check(kal::fs::open_file(d, "inside", 6, kal::fs::open::write | kal::fs::open::create, &f) == kal_ok, + "a file is created in it"); + kal_stream_write(kal_fs_stream(f), "x", 1); + kal_fs_close_file(f); + kal_fs_close_dir(d); + + check(::chmod("okl_search_only", 0111) == 0, "the directory loses its read right"); + + kal_dir s{}; + check(kal_fs_open_dir(work, "okl_search_only", 15, &s) == kal_ok, + "a searchable directory without the read right opens as a base"); + kal_file g{}; + check(kal::fs::open_file(s, "inside", 6, kal::fs::open::read, &g) == kal_ok, + "a name beneath it resolves"); + char c = 0; + check(kal_stream_read(kal_fs_stream(g), &c, 1) == 1 && c == 'x', "the file beneath it reads"); + kal_fs_close_file(g); + + // A program is started IN it: the base it is resolved under is the root + // preopen, and its working directory is the search-only directory. + const char* argv[] = { "true" }; + const kal_uintptr lens[] = { 4 }; + const kal_spawn how{ root(), s, nullptr, nullptr, 0, 0 }; + kal_process p{}; + const int rc = kal_process_spawn(&how, "bin/true", 8, argv, lens, 1, nullptr, nullptr, 0, nullptr, &p); + check(rc == kal_ok, "a program starts in a search-only directory"); + if (rc == kal_ok) { + int status = -1, terminated = -1; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); + check(terminated == 0 && status == 0, "the program ran to completion"); + } + + if (::geteuid() != 0) { + kal_uintptr iter = 0; + check(kal_fs_list_begin(s, &iter) == kal_err_permission, + "listing it is what the missing read right refuses"); + } + kal_fs_close_dir(s); + ::chmod("okl_search_only", 0755); + ::unlink("okl_search_only/inside"); + ::rmdir("okl_search_only"); + + if (failures == 0) std::printf("search_only_base: ok\n"); + return failures == 0 ? 0 : 1; +}