diff --git a/README.md b/README.md index 366b973..594b5c9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface. openkal = "0.23.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.24.0" +openkal-linux = "0.24.1" ``` ## Why it does not use a C library @@ -124,11 +124,22 @@ to its record in batches, and `e\0` at `kal_exit` or the C library's destructors. A grant carries its rights in `KAL_GRANT_RIGHTS`, and its record as a descriptor after the grants. -**Confinement** (`KAL_SPAWN_CONFINE`, `KAL_PROCESS_PROP_CONFINE`): Landlock, at -the ABI version the kernel reports, set in the started image before it is -replaced. The grants are the rules, by their rights; the program's own file, +**Confinement** (`KAL_SPAWN_CONFINE`, `KAL_PROCESS_PROP_CONFINE`): Landlock ABI 3 +or later, set in the started image before it is replaced. Earlier ABIs cannot +restrict file truncation, so confinement is not claimed and a request is +refused before starting a program. The grants are the rules, by their rights; the program's own file, `/proc` and `/dev/null` are added, and `PR_SET_NO_NEW_PRIVS` is set. +## What 0.24.1 fixes + +Confinement requires Landlock ABI 3, which can restrict both `truncate(2)` and +`open(2)` with `O_RDONLY | O_TRUNC`. ABI 1 and 2 restrict opening a file for +writing but cannot prevent these changes to a file in a read-only grant. +The ABI cache uses atomic accesses when several threads first query process +properties or start a program concurrently. The regression test opens an +existing granted file for reading, attempts both raw truncation operations, +and checks that its length remains unchanged. + **Observation.** A fault is reported on an alternate stack from `/proc/self/maps` and the ELF symbol tables, and the program is then ended as the kernel ends it. Samples are `ITIMER_PROF` and `SIGPROF` into a ring a diff --git a/mcpp.toml b/mcpp.toml index 62b5598..e97726b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.24.0" +version = "0.24.1" 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 de72b54..c0cc485 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -536,13 +536,19 @@ namespace { // else. A ruleset of the grants, each with what its rights allow; the program // itself, to execute it; /proc, which the program's own implementation reads // (`kal_fs_resolved_name'); and /dev/null, which a null position is. +// ABI 1/2 cannot restrict truncate(2), including O_RDONLY | O_TRUNC. +// A read-only grant therefore requires ABI 3 before CONFINE can be claimed. +constexpr int kConfinementAbi = 3; + int landlock_abi() { static int abi = -2; - if (abi == -2) { + int value = __atomic_load_n(&abi, __ATOMIC_ACQUIRE); + if (value == -2) { const okl_long r = okl::sys(okl::nr_landlock_create_ruleset, 0, 0, 1 /* _VERSION */); - abi = okl::failed(r) ? 0 : static_cast(r); + value = okl::failed(r) ? 0 : static_cast(r); + __atomic_store_n(&abi, value, __ATOMIC_RELEASE); } - return abi; + return value; } okl_u64 handled_for(int abi) { @@ -557,7 +563,7 @@ okl_u64 handled_for(int abi) { // replaced. Returns zero, or the kernel's negated reason. okl_long confine_to(const okl_u32* rights, kal_uintptr g, int base, const char* program) { const int abi = landlock_abi(); - if (abi < 1) return -okl::e_nosys; + if (abi < kConfinementAbi) return -okl::e_nosys; struct { okl_u64 fs; } attr{ handled_for(abi) }; const okl_long rs = okl::sys(okl::nr_landlock_create_ruleset, reinterpret_cast(&attr), sizeof attr, 0); @@ -915,7 +921,7 @@ int kal_process_spawn(const kal_spawn* how, const kal_uintptr preference = how->flags & preferences; if ((preference & (preference - 1)) != 0) return kal_err_invalid; const bool confine = (how->flags & KAL_SPAWN_CONFINE) != 0; - if (confine && landlock_abi() < 1) return kal_err_not_supported; + if (confine && landlock_abi() < kConfinementAbi) return kal_err_not_supported; // THE NULL DEVICE BESIDE A STREAM AT ONE POSITION is two answers to one // question (openkal 0.22, clause 7.15). if (streams != nullptr @@ -1608,7 +1614,7 @@ kal_uintptr kal_process_props(void) { | KAL_PROCESS_PROP_JOB | KAL_PROCESS_PROP_JOB_USAGE | KAL_PROCESS_PROP_JOB_LIMIT_SUPERVISED | KAL_PROCESS_PROP_NULL_STREAMS - | (landlock_abi() >= 1 ? KAL_PROCESS_PROP_CONFINE : 0u) + | (landlock_abi() >= kConfinementAbi ? KAL_PROCESS_PROP_CONFINE : 0u) | (open_id_available() || stat_state(0) != 0 ? KAL_PROCESS_PROP_OPEN_ID : 0u) // AGREES WITH `kal_process_stop_requested', because the header // defines null there as the absence this position reports. Read and diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp index 244ad56..74ff112 100644 --- a/tests/openkal_0_23.cpp +++ b/tests/openkal_0_23.cpp @@ -6,11 +6,13 @@ // is refused by Landlock where the kernel has it. A PREFERENCE FOR ECONOMY // turns the program's transparent huge pages off. #include +#include #include #include #include #include #include +#include #include import openkal.types; import openkal.env; @@ -35,13 +37,14 @@ kal_dir root() { char g_self[4096]; char g_dir[] = "/tmp/okl-0.24-XXXXXX"; -int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags) { +int start(const char* errand, const kal_preopen* grants, kal_uintptr n, kal_uintptr flags, + const char* operand = nullptr) { const kal_dir r = root(); - const char* args[] = { g_self, errand }; - const kal_uintptr lens[] = { std::strlen(g_self), std::strlen(errand) }; + const char* args[] = { g_self, errand, operand }; + const kal_uintptr lens[] = { std::strlen(g_self), std::strlen(errand), operand ? std::strlen(operand) : 0 }; const kal_spawn how{ r, r, nullptr, grants, n, flags }; kal_process p{}; - if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, 2, nullptr, nullptr, 0, + if (kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), args, lens, operand ? 3 : 2, nullptr, nullptr, 0, nullptr, &p) != kal_ok) return -1; int status = -1, terminated = 0; @@ -75,6 +78,30 @@ int main(int argc, char** argv) { } if (argc > 1 && std::strcmp(argv[1], "--thp") == 0) return static_cast(::prctl(PR_GET_THP_DISABLE, 0, 0, 0, 0)) == 1 ? 70 : 71; + if (argc == 3 && (std::strcmp(argv[1], "--truncate-raw") == 0 || + std::strcmp(argv[1], "--open-truncate-raw") == 0)) { + // Prove the existing file is readable before asking the kernel to + // truncate it. Missing files and unreachable grants are not proof. + const int readable = static_cast(::syscall(SYS_openat, AT_FDCWD, argv[2], O_RDONLY)); + if (readable < 0) return 66; + ::close(readable); + errno = 0; + const long result = std::strcmp(argv[1], "--truncate-raw") == 0 + ? ::syscall(SYS_truncate, argv[2], 0) + : ::syscall(SYS_openat, AT_FDCWD, argv[2], O_RDONLY | O_TRUNC); + const int why = errno; + if (result >= 0) { + if (std::strcmp(argv[1], "--open-truncate-raw") == 0) ::close(static_cast(result)); + return 64; + } + return why == EACCES ? 63 : 65; + } + + // The pinned compiler's Linux headers can predate Landlock. This syscall + // has number 444 on both architectures this implementation builds for. + const long landlock = ::syscall(444, nullptr, 0, 1 /* _VERSION */); + check(kal::process::has(kal::process::confine) == (landlock >= 3), + "confinement is claimed only where Landlock can restrict file truncation"); const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } @@ -138,8 +165,17 @@ int main(int argc, char** argv) { const int s = start("--write-raw", grants, count, kal::process::confine_flag.bits); check(s == 60, "a confined program is refused by the kernel what its grants do not allow, without openkal"); if (s != 60) std::printf(" the confined program reported %d\n", s); + std::snprintf(path, sizeof path, "%s/present.txt", g_dir); + const char* truncations[] = { "--truncate-raw", "--open-truncate-raw" }; + for (const char* errand : truncations) { + check(start(errand, grants, count, kal::process::confine_flag.bits, path) == 63, + "a readable file in a read-only grant cannot be truncated without openkal"); + struct stat info{}; + check(::stat(path, &info) == 0 && info.st_size == 1, + "the read-only file retains its contents after the raw truncation attempt"); + } } else { - std::printf(" this kernel has no Landlock; confinement is refused, which the conformance suite observes\n"); + std::printf(" this kernel has no Landlock ABI 3; confinement is refused, which the conformance suite observes\n"); } // Economy.