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

Expand Down
18 changes: 12 additions & 6 deletions src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(r);
value = okl::failed(r) ? 0 : static_cast<int>(r);
__atomic_store_n(&abi, value, __ATOMIC_RELEASE);
}
return abi;
return value;
}

okl_u64 handled_for(int abi) {
Expand All @@ -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<okl_long>(&attr),
sizeof attr, 0);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
46 changes: 41 additions & 5 deletions tests/openkal_0_23.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cstdio>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <fcntl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <unistd.h>
import openkal.types;
import openkal.env;
Expand All @@ -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;
Expand Down Expand Up @@ -75,6 +78,30 @@ int main(int argc, char** argv) {
}
if (argc > 1 && std::strcmp(argv[1], "--thp") == 0)
return static_cast<int>(::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<int>(::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<int>(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; }
Expand Down Expand Up @@ -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.
Expand Down
Loading