diff --git a/README.md b/README.md index 76e396c..366b973 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ for Linux, written on the kernel's own system-call interface. ```toml [dependencies] -openkal = "0.22.0" +openkal = "0.23.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.23.0" +openkal-linux = "0.24.0" ``` ## Why it does not use a C library @@ -102,6 +102,43 @@ A program that carries a runtime of its own answers from that runtime instead (`pthread_getattr_np`), because in that arrangement the runtime is what owns the stacks and is what a program above openkal would ask for itself. +## What 0.24 adds (openkal 0.23) + +**A region at an address.** `kal_exec_alloc_at` maps the region with +`MAP_FIXED_NOREPLACE`, which refuses rather than replaces what is there; a kernel +before 4.17 reads the flag as a hint, so a mapping placed elsewhere is released +and the request refused. `KAL_EXEC_PROP_PLACE` is claimed. + +**A part sealed, a part read in.** `kal_exec_seal_part` is `mprotect` to read +only, and `mseal` for `KAL_EXEC_SEAL_FOREVER` where the kernel has it (6.10; +`KAL_EXEC_PROP_SEAL_FOREVER` is claimed by asking it once). `KAL_EXEC_MAP_PREFETCH` +is `MAP_POPULATE`; `KAL_EXEC_MAP_BTI` is `PROT_BTI` on arm64, claimed where the +kernel accepts it. + +**A handle with less** (`kal_fs_dir_attenuate`): a table indexed by descriptor, +consulted by every operation of `openkal.fs` that takes a directory and empty in +a program that attenuates nothing. A read-only handle refuses every change; one +that states nothing changes answers absence from a listing it has read, within +a budget the resource preference sets; a recorded one appends `\0` +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, +`/proc` and `/dev/null` are added, and `PR_SET_NO_NEW_PRIVS` is set. + +**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 +handler writes without a lock. Touches: a part recorded is made readable and +not executable, and the handler makes each page executable again at its first +execution and records it; a fault that is not a touch is reported, or passed +to the program's own handler. `kal_process_info` reports minor and major +faults (`getrusage`, `/proc//stat`). `kal_memory_info` is `MemAvailable` +within the cgroup's `memory.max`; economy sets `PR_SET_THP_DISABLE`. + ## What 0.23 adds (openkal 0.22) **The null device at a position.** `KAL_SPAWN_NULL_IN`, `KAL_SPAWN_NULL_OUT` and diff --git a/mcpp.toml b/mcpp.toml index 7eb3bad..62b5598 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.23.0" +version = "0.24.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" @@ -62,10 +62,12 @@ provides-interfaces = [ "openkal.timeout", "openkal.map", "openkal.region", + "openkal.prof", + "openkal.debug", ] [dependencies] -openkal = "0.22.0" +openkal = "0.23.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/src/abort.cpp b/src/abort.cpp index f4e23bf..efdcfb2 100644 --- a/src/abort.cpp +++ b/src/abort.cpp @@ -1,4 +1,5 @@ #include "sys.h" +#include "rights.h" #include extern "C" { @@ -20,6 +21,9 @@ extern "C" { // destructors first --- is not observable in a small program and is observable // in a large one, which is why it is stated rather than left to judgement. [[noreturn]] void kal_exit(int code) { + // What the program's attenuated handles recorded is written, and ended + // (0.23, fs.h): a record without its end is of a program that did not end. + okl::rights::end_of_program(); for (;;) okl::sys(okl::nr_exit_group, code); } diff --git a/src/env.cpp b/src/env.cpp index 71790ac..03c2bb0 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -1,6 +1,7 @@ #include "sys.h" #include +namespace okl { int resource_preference(); } namespace okl { // The vectors the kernel places on the stack at inception. They are recorded @@ -252,4 +253,45 @@ kal_intptr kal_env_system(char* out, kal_uintptr cap) { return give("linux", 5, out, cap); } +// THE RESOURCE PREFERENCE (0.23), which a start conveys in KAL_RESOURCES +// (process.cpp) and a person sets the same way. +int kal_env_resources(void) { return okl::resource_preference(); } + +} + +namespace okl { +int resource_preference() { + static int known = -1; + int v = __atomic_load_n(&known, __ATOMIC_RELAXED); + if (v >= 0) return v; + v = 0; + constexpr char key[] = "KAL_RESOURCES="; + constexpr okl_uptr key_len = sizeof key - 1; + for (char** e = okl::g_envp; e && *e; ++e) { + okl_uptr i = 0; + while (i < key_len && (*e)[i] == key[i]) ++i; + if (i != key_len) continue; + const char* val = *e + key_len; + auto is = [&](const char* w) { + okl_uptr k = 0; + while (w[k] && val[k] == w[k]) ++k; + return w[k] == '\0' && val[k] == '\0'; + }; + v = is("performance") ? 1 : is("economy") ? 2 : 0; + break; + } + __atomic_store_n(&known, v, __ATOMIC_RELAXED); + return v; } +} // namespace okl + +namespace { +// THE ENVIRONMENT'S OWN POLICY FOR MEMORY, AS THE PREFERENCE ASKS (0.23): a +// program started for economy has no transparent huge pages, which hold a whole +// two mebibytes resident for the first byte touched. After `capture', which +// reads the environment. +[[gnu::constructor(102)]] void apply_resource_preference() { + if (okl::resource_preference() == 2) + okl::sys(okl::nr_prctl, 41 /* PR_SET_THP_DISABLE */, 1, 0, 0, 0); +} +} // namespace diff --git a/src/exec.cpp b/src/exec.cpp index b91f1d8..36208c0 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -39,6 +39,25 @@ okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); } } // namespace +namespace { +// Whether this kernel has `mseal': it answers a range of no length with zero, +// and a kernel without it ENOSYS. +bool sealing() { + static int known = -1; + if (known < 0) known = okl::failed(okl::sys(okl::nr_mseal, 0, 0, 0)) ? 0 : 1; + return known == 1; +} +// Whether instructions mapped PROT_BTI are held to branch targets: the +// processor states it in AT_HWCAP2 (HWCAP2_BTI), and only on aarch64. +bool branch_targets() { +#if defined(__aarch64__) + return (okl::auxval(26 /* AT_HWCAP2 */) & (okl_ulong{1} << 17)) != 0; +#else + return false; +#endif +} +} // namespace + extern "C" { void* kal_exec_alloc(kal_uintptr size) { @@ -51,6 +70,26 @@ void* kal_exec_alloc(kal_uintptr size) { return reinterpret_cast(r); } +// AT AN ADDRESS (0.23): MAP_FIXED_NOREPLACE, which refuses rather than replaces +// what is there. A kernel before 4.17 does not know the flag and reads the +// address as a hint, so the answer is compared and a mapping placed elsewhere +// released: the address is either given or refused, never approximated. +void* kal_exec_alloc_at(void* at, kal_uintptr size) { + const okl_uptr where = reinterpret_cast(at); + if (size == 0 || where == 0 || (where & (page() - 1)) != 0) return nullptr; + const okl_uptr bytes = round_up(static_cast(size), page()); + const okl_long r = okl::sys(okl::nr_mmap, static_cast(where), static_cast(bytes), + okl::prot_read | okl::prot_write, + okl::map_private | okl::map_anonymous | 0x100000 /* MAP_FIXED_NOREPLACE */, + -1, 0); + if (okl::failed(r)) return nullptr; + if (static_cast(r) != where) { + okl::sys(okl::nr_munmap, r, static_cast(bytes)); + return nullptr; + } + return at; +} + int kal_exec_publish(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return kal_err_invalid; const okl_uptr bytes = round_up(static_cast(size), page()); @@ -114,6 +153,25 @@ int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { return okl::failed(r) ? okl::translate(r) : kal_ok; } +// SEALED READ-ONLY (0.23): the protection, and, for ever, `mseal' (Linux 6.10) +// over it, after which the kernel refuses to change the part's protection or to +// unmap it --- `mprotect' and `munmap' of it report EPERM, which this +// implementation reports as kal_err_permission. +int kal_exec_seal_part(void* region, kal_uintptr offset, kal_uintptr size, kal_uintptr flags) { + const okl_uptr g = page(); + if (region == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 + || (reinterpret_cast(region) & (g - 1)) != 0 || (flags & ~KAL_EXEC_SEAL_FOREVER) != 0) + return kal_err_invalid; + const okl_long at = reinterpret_cast(static_cast(region) + offset); + const okl_long r = okl::sys(okl::nr_mprotect, at, static_cast(size), okl::prot_read); + if (okl::failed(r)) return okl::translate(r); + if ((flags & KAL_EXEC_SEAL_FOREVER) && sealing()) { + const okl_long m = okl::sys(okl::nr_mseal, at, static_cast(size), 0); + if (okl::failed(m)) return okl::translate(m); + } + return kal_ok; +} + kal_uintptr kal_exec_granularity(void) { return page(); } // A PART THAT IS A FILE'S (0.21): the file's pages mapped over the part, @@ -127,7 +185,8 @@ int kal_exec_map_part(void* region, kal_uintptr offset, kal_uintptr size, kal_fi const int fd = okl::unpack(f.h); if (region == nullptr || size == 0 || fd < 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 || (reinterpret_cast(region) & (g - 1)) != 0 || at % KAL_MAP_ALIGNMENT != 0 - || (how & ~KAL_EXEC_MAP_EXECUTE) != 0) + || (how & ~(KAL_EXEC_MAP_EXECUTE | KAL_EXEC_MAP_PREFETCH | KAL_EXEC_MAP_BTI | KAL_EXEC_MAP_WRITE)) != 0 + || ((how & KAL_EXEC_MAP_WRITE) && (how & KAL_EXEC_MAP_EXECUTE))) return kal_err_invalid; okl::kstat st{}; const okl_long s = okl::sys(okl::nr_fstat, fd, reinterpret_cast(&st)); @@ -135,10 +194,20 @@ int kal_exec_map_part(void* region, kal_uintptr offset, kal_uintptr size, kal_fi if ((st.mode & okl::s_ifmt) != okl::s_ifreg) return kal_err_not_supported; const kal_u64 length = st.size > 0 ? static_cast(st.size) : 0; if (at > length || static_cast(size) > length - at) return kal_err_invalid; - const okl_long prot = okl::prot_read | ((how & KAL_EXEC_MAP_EXECUTE) ? okl::prot_exec : 0); + // READ IN NOW (0.23): MAP_POPULATE reads the part and fills its page + // tables in the one call, where each page would otherwise be a fault of its + // own on its first touch. BRANCH TARGETS: PROT_BTI, where the processor + // holds them. + const bool bti = (how & KAL_EXEC_MAP_BTI) && (how & KAL_EXEC_MAP_EXECUTE) && branch_targets(); + // WRITTEN PRIVATELY (0.23): the mapping is private already, so a page the + // program writes is its own copy and the file is not changed. + const okl_long prot = okl::prot_read | ((how & KAL_EXEC_MAP_EXECUTE) ? okl::prot_exec : 0) + | ((how & KAL_EXEC_MAP_WRITE) ? okl::prot_write : 0) + | (bti ? 0x10 /* PROT_BTI */ : 0); + const okl_long populate = (how & KAL_EXEC_MAP_PREFETCH) ? 0x8000 /* MAP_POPULATE */ : 0; const okl_long r = okl::sys(okl::nr_mmap, reinterpret_cast(static_cast(region) + offset), - static_cast(size), prot, okl::map_private | okl::map_fixed, fd, + static_cast(size), prot, okl::map_private | okl::map_fixed | populate, fd, static_cast(at)); if (okl::failed(r)) { // ENODEV: the file system does not map. EPERM: its files are not @@ -163,7 +232,8 @@ void kal_exec_free(void* p, kal_uintptr size) { kal_uintptr kal_exec_props(void) { // Executable memory is available to every artifact on this kernel: nothing // here is granted only to a program produced in a particular way. - return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE | KAL_EXEC_PROP_FILE; + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE | KAL_EXEC_PROP_FILE | KAL_EXEC_PROP_PLACE + | (sealing() ? KAL_EXEC_PROP_SEAL_FOREVER : 0u) | (branch_targets() ? KAL_EXEC_PROP_BTI : 0u); } } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index 0cc5ce9..51839c6 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -1,5 +1,6 @@ #include "sys.h" #include "handle.h" +#include "rights.h" #include #include @@ -7,6 +8,20 @@ namespace okl { extern char** g_envp; } namespace { +// THE RIGHTS OF A DIRECTORY HANDLE (0.23, rights.h): an operation that would +// change something beneath a read-only handle is refused before it is asked of +// the kernel; one that did change something is recorded, and what a handle that +// states nothing changes has listed no longer holds. +inline bool refused_write(int b) { return (okl::rights::of(b) & okl::rights::read_only) != 0; } +inline void wrote(int b, const char* n, kal_uintptr l) { + if (!okl::rights::g_any) return; + okl::rights::changed(b); + okl::rights::note(b, 'w', n, l); +} +inline void asked(int b, bool present, const char* n, kal_uintptr l) { + if (okl::rights::g_any) okl::rights::note(b, present ? 's' : 'a', n, l); +} + // The directories this implementation supplies. A hosted system does not // confine an ordinary program, so it supplies both the working directory and @@ -42,6 +57,50 @@ constexpr kal_uintptr kMaxGrants = 16; // // Answers false when there is no such value, which leaves the directories this // implementation supplies by default. +// WHAT A GRANT CARRIES (0.23): the rights and statements of an attenuated +// handle, its record, and where it lies beneath the directory the record is of, +// in a second variable a start writes beside the first, +// +// KAL_GRANT_RIGHTS={;,,,,} +// +// read when this process is `'. A program built before it ignores it and +// holds its grants without rights; one started confined is held to them by the +// kernel in any case (process.cpp). +void adopt_rights(const preopen* t, kal_uintptr count) { + constexpr char key[] = "KAL_GRANT_RIGHTS="; + constexpr okl_uptr key_len = sizeof key - 1; + const char* v = nullptr; + for (char** e = okl::g_envp; e && *e; ++e) { + okl_uptr i = 0; + while (i < key_len && (*e)[i] == key[i]) ++i; + if (i == key_len) { v = *e + key_len; break; } + } + if (v == nullptr) return; + auto number = [&](long& out) { + bool neg = false; + if (*v == '-') { neg = true; ++v; } + if (*v < '0' || *v > '9') return false; + out = 0; + while (*v >= '0' && *v <= '9') out = out * 10 + (*v++ - '0'); + if (neg) out = -out; + return true; + }; + long pid = 0; + if (!number(pid) || pid != okl::sys(okl::nr_getpid)) return; + while (*v == ';') { + ++v; + long index = 0, flags = 0, record = -1, len = 0; + if (!number(index) || *v++ != ',' || !number(flags) || *v++ != ',' || !number(record) + || *v++ != ',' || !number(len) || *v++ != ',' || len < 0) + return; + for (long i = 0; i < len; ++i) if (v[i] == '\0') return; + if (index >= 0 && static_cast(index) < count && t[index].handle != 0) + okl::rights::adopt(okl::unpack(t[index].handle), static_cast(flags), + static_cast(record), v, static_cast(len)); + v += len; + } +} + bool granted(preopen* t, kal_uintptr* n) { constexpr char key[] = "KAL_PREOPENS="; constexpr okl_uptr key_len = sizeof key - 1; @@ -79,6 +138,7 @@ bool granted(preopen* t, kal_uintptr* n) { } if (*v != '\0') return false; *n = k; + adopt_rights(t, k); return true; } @@ -469,9 +529,24 @@ int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* ou const int b = okl::unpack(base.h); 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_u32 rights = okl::rights::of(b); + if ((rights & okl::rights::unchanging) && okl::rights::known_absent(b, name, len)) { + asked(b, false, name, len); + return kal_err_not_found; + } const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), dir_handle_flags, 0); - if (okl::failed(fd)) return okl::translate(fd); + if (okl::failed(fd)) { + if (rights && (fd == -okl::e_noent || fd == -okl::e_notdir)) asked(b, false, name, len); + return okl::translate(fd); + } + // A directory opened beneath a handle with rights has them; where the table + // has no room the directory is not handed out with more than its base. + if (rights && !okl::rights::derive(b, static_cast(fd), name, len)) { + okl::sys(okl::nr_close, fd); + return kal_err_no_memory; + } + if (rights) asked(b, true, name, len); *out = kal_dir{ okl::pack(static_cast(fd)) }; return kal_ok; } @@ -491,8 +566,23 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, if (flags & KAL_OPEN_TRUNCATE) f |= okl::o_trunc; if (flags & KAL_OPEN_APPEND) f |= okl::o_append; + const okl_u32 rights = okl::rights::of(b); + const bool changes = (flags & (KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE | KAL_OPEN_APPEND)) != 0; + if (changes && (rights & okl::rights::read_only)) return kal_err_permission; + if (!(flags & KAL_OPEN_CREATE) && (rights & okl::rights::unchanging) + && okl::rights::known_absent(b, name, len)) { + asked(b, false, name, len); + return kal_err_not_found; + } const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), f, 0666); - if (okl::failed(fd)) return okl::translate(fd); + if (okl::failed(fd)) { + if (rights && (fd == -okl::e_noent || fd == -okl::e_notdir)) asked(b, false, name, len); + return okl::translate(fd); + } + if (rights) { + if (changes) wrote(b, name, len); + else okl::rights::note(b, 'r', name, len); + } *out = kal_file{ okl::pack(static_cast(fd)) }; return kal_ok; } @@ -503,6 +593,7 @@ void kal_fs_close_dir(kal_dir d) { // Before the close, because after it the number may already name another // directory whose enumerations are not this one's. listings_end(fd); + okl::rights::forget(fd); okl::retire(d.h); okl::sys(okl::nr_close, fd); } @@ -584,6 +675,12 @@ int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, const int b = okl::unpack(base.h); if (b < 0 || !info_ok(out)) return kal_err_invalid; okl::accepted_name t(name, len); if (!t.ok) return kal_err_invalid; + const okl_u32 rights = okl::rights::of(b); + if ((rights & okl::rights::unchanging) && okl::rights::known_absent(b, name, len)) { + asked(b, false, name, len); + fill_absent(out); + return kal_ok; + } okl::kstat st{}; // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. // @@ -601,11 +698,13 @@ int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, // same answer, because the name still refers to nothing --- and so is a // node whose content names something absent, when the enquiry resolves. if (r == -okl::e_noent || r == -okl::e_notdir || r == -okl::e_loop) { + if (rights) asked(b, false, name, len); fill_absent(out); return kal_ok; } return okl::translate(r); } + if (rights) asked(b, true, name, len); fill_info(st, wanted, out, b); return kal_ok; } @@ -652,6 +751,7 @@ int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, const int b = okl::unpack(base.h); if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + if (refused_write(b)) return kal_err_permission; constexpr okl_i64 utime_omit = 0x3ffffffe; okl::ktimespec times[2]; @@ -663,7 +763,9 @@ int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, const okl_long r = okl::sys(okl::nr_utimensat, b, reinterpret_cast(t.buf), reinterpret_cast(times), 0); - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(b, name, len); + return kal_ok; } // Whether a node may be started, version 0.13. This kernel stores the property @@ -676,6 +778,7 @@ int kal_fs_set_executable_at(kal_dir base, const char* name, kal_uintptr len, if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; if ((volume_props(b) & KAL_FS_PROP_EXECUTABLE) == 0) return kal_err_not_supported; + if (refused_write(b)) return kal_err_permission; okl::kstat st{}; okl_long r = okl::sys(okl::nr_newfstatat, b, reinterpret_cast(t.buf), @@ -692,29 +795,35 @@ int kal_fs_set_executable_at(kal_dir base, const char* name, kal_uintptr len, // Resolves, as the enquiry above did, so the node changed is the node read. r = okl::sys(okl::nr_fchmodat, b, reinterpret_cast(t.buf), static_cast(next)); - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(b, name, len); + return kal_ok; } int kal_fs_mkdir(kal_dir base, const char* name, kal_uintptr len) { const int b = okl::unpack(base.h); if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + if (refused_write(b)) return kal_err_permission; const okl_long r = okl::sys(okl::nr_mkdirat, b, reinterpret_cast(t.buf), 0777); - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(b, name, len); + return kal_ok; } int kal_fs_remove(kal_dir base, const char* name, kal_uintptr len) { const int b = okl::unpack(base.h); if (b < 0 || !okl::acceptable(name, len)) return kal_err_invalid; okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + if (refused_write(b)) return kal_err_permission; okl_long r = okl::sys(okl::nr_unlinkat, b, reinterpret_cast(t.buf), 0); - if (!okl::failed(r)) return kal_ok; + if (!okl::failed(r)) { wrote(b, name, len); return kal_ok; } // One operation removes a name, and the kernel distinguishes two kinds of // name where this interface does not. if (r == -okl::e_isdir || r == -okl::e_perm) { const okl_long d = okl::sys(okl::nr_unlinkat, b, reinterpret_cast(t.buf), okl::at_removedir); - if (!okl::failed(d)) return kal_ok; + if (!okl::failed(d)) { wrote(b, name, len); return kal_ok; } r = d; } return okl::translate(r); @@ -726,9 +835,13 @@ int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, if (f < 0 || t2 < 0 || !okl::acceptable(a, alen) || !okl::acceptable(b, blen)) return kal_err_invalid; okl::terminated ta(a, alen), tb(b, blen); if (!ta.ok || !tb.ok) return kal_err_invalid; + if (refused_write(f) || refused_write(t2)) return kal_err_permission; const okl_long r = okl::sys(okl::nr_renameat, f, reinterpret_cast(ta.buf), t2, reinterpret_cast(tb.buf)); - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(f, a, alen); + wrote(t2, b, blen); + return kal_ok; } // A RENAME THAT DOES NOT REPLACE (0.17) is the kernel's own: `renameat2' with @@ -746,10 +859,14 @@ int kal_fs_rename_flags(kal_dir from, const char* a, kal_uintptr alen, if ((volume_props(t2) & KAL_FS_PROP_RENAME_NO_REPLACE) == 0) return kal_err_not_supported; okl::terminated ta(a, alen), tb(b, blen); if (!ta.ok || !tb.ok) return kal_err_invalid; + if (refused_write(f) || refused_write(t2)) return kal_err_permission; const okl_long r = okl::sys(okl::nr_renameat2, f, reinterpret_cast(ta.buf), t2, reinterpret_cast(tb.buf), 1 /* RENAME_NOREPLACE */); if (r == -okl::e_inval || r == -okl::e_nosys) return kal_err_not_supported; - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(f, a, alen); + wrote(t2, b, blen); + return kal_ok; } // Enumeration holds a descriptor of its own, obtained by opening the directory @@ -768,6 +885,7 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { s->used = 0; s->pos = 0; listings_lock(); s->next = g_listings; g_listings = s; listings_unlock(); *iter = reinterpret_cast(s); + if (okl::rights::g_any) okl::rights::note(fd, 'l', "", 0); return kal_ok; } @@ -825,8 +943,28 @@ int kal_fs_list_next(kal_dir d, kal_uintptr* iter, // filesystem it mounts, and `renameat' within one directory is atomic by POSIX. // Case sensitivity and links are claimed only where the format is known to have // them. +// WHETHER THE KERNEL NAMES A DESCRIPTOR (/proc/self/fd), which a name with +// every link followed is read from (0.23). Asked once: /proc is mounted for the +// process or it is not, whatever the volume. +static bool descriptor_names() { + static int known = -1; + if (known < 0) { + char b[8]; + known = okl::failed(okl::sys(okl::nr_readlinkat, okl::at_fdcwd, + reinterpret_cast("/proc/self/cwd"), + reinterpret_cast(b), static_cast(sizeof b))) ? 0 : 1; + } + return known == 1; +} + kal_uintptr kal_fs_props(kal_dir d) { - return volume_props(okl::unpack(d.h)); + const int fd = okl::unpack(d.h); + if (fd < 0) return 0; + const okl_u32 rights = okl::rights::of(fd); + return volume_props(fd) | (descriptor_names() ? KAL_FS_PROP_RESOLVED_NAME : 0u) + | KAL_FS_PROP_ATTENUATE + | ((rights & okl::rights::read_only) ? KAL_FS_PROP_READ_ONLY : 0u) + | ((rights & okl::rights::unchanging) ? KAL_FS_PROP_UNCHANGING : 0u); } // --- exclusion upon a range of a file -------------------------------------- @@ -933,9 +1071,54 @@ int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, if (b < 0 || !okl::acceptable(name, len) || target == nullptr) return kal_err_invalid; okl::terminated n(name, len); if (!n.ok) return kal_err_invalid; okl::terminated tgt(target, target_len); if (!tgt.ok) return kal_err_invalid; + if (refused_write(b)) return kal_err_permission; const okl_long r = okl::sys(okl::nr_symlinkat, reinterpret_cast(tgt.buf), b, reinterpret_cast(n.buf)); - return okl::failed(r) ? okl::translate(r) : kal_ok; + if (okl::failed(r)) return okl::translate(r); + wrote(b, name, len); + return kal_ok; +} + +// THE NAME WITH EVERY LINK FOLLOWED (0.23): the node opened for its path alone +// (O_PATH, which needs no permission upon it and follows every link), and the +// kernel's record of that descriptor's name read, as this system's own C +// library answers `realpath' of an open file. Two calls and a close, where a +// walk asks about every component of the name. +kal_intptr kal_fs_resolved_name(kal_dir base, const char* name, kal_uintptr len, + char* out, kal_uintptr cap) { + const int b = okl::unpack(base.h); + if (b < 0 || !okl::acceptable(name, len)) return -kal_err_invalid; + if (!descriptor_names()) return -kal_err_not_supported; + okl::terminated t(name, len); if (!t.ok) return -kal_err_invalid; + const okl_u32 rights = okl::rights::of(b); + if ((rights & okl::rights::unchanging) && okl::rights::known_absent(b, name, len)) { + asked(b, false, name, len); + return -kal_err_not_found; + } + const okl_long fd = okl::sys(okl::nr_openat, b, reinterpret_cast(t.buf), + okl::o_path | okl::o_cloexec, 0); + if (okl::failed(fd)) { + if (rights && (fd == -okl::e_noent || fd == -okl::e_notdir)) asked(b, false, name, len); + return -okl::translate(fd); + } + if (rights) asked(b, true, name, len); + char link[32] = "/proc/self/fd/"; + { + okl_uptr at = 14; + char digits[20]; int n = 0; + okl_ulong v = static_cast(fd); + do { digits[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (n) link[at++] = digits[--n]; + link[at] = '\0'; + } + char own[2 * okl::max_name + 2]; + const okl_long r = okl::sys(okl::nr_readlinkat, okl::at_fdcwd, reinterpret_cast(link), + reinterpret_cast(own), static_cast(sizeof own)); + okl::sys(okl::nr_close, fd); + if (okl::failed(r)) return -okl::translate(r); + if (out != nullptr) + for (okl_long i = 0; i < r && static_cast(i) < cap; ++i) out[i] = own[i]; + return static_cast(r); } kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len, @@ -967,4 +1150,65 @@ kal_intptr kal_fs_link_read(kal_dir base, const char* name, kal_uintptr len, return static_cast(r); } +// THE LISTING WITH WHAT IT TOLD OF THE ENTRY (0.23). This kernel's listing +// gives a name and a kind and nothing else, so a field beyond the kind is asked +// of the entry itself --- the same request a caller would make, made here so +// that the caller crosses this interface once. KAL_FS_PROP_LIST_INFO is not +// claimed: the listing does not carry the attributes. +int kal_fs_list_next_info(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, + kal_uintptr* name_len, kal_u32 wanted, kal_node_info* info) { + if (!info_ok(info)) return kal_err_invalid; + char own[okl::max_name + 1]; + kal_uintptr n = 0; + int kind = 0; + const int e = kal_fs_list_next(d, iter, own, sizeof own - 1, &n, &kind); + if (e != kal_ok) return e; + if (name_len) *name_len = n; + if (name_out && name_cap) okl::copy(name_out, own, n < name_cap ? n : name_cap); + const kal_u32 self = info->self_size; + if (*iter == 0 && n == 0) { fill_absent(info); return kal_ok; } + own[n < sizeof own ? n : sizeof own - 1] = '\0'; + kal_node_info v{}; + v.self_size = self; + v.kind = kind; + v.present = KAL_INFO_KIND; + if ((wanted & ~KAL_INFO_KIND) != 0) { + okl::kstat st{}; + const int fd = okl::unpack(d.h); + if (!okl::failed(okl::sys(okl::nr_newfstatat, fd, reinterpret_cast(own), + reinterpret_cast(&st), okl::at_symlink_nofollow))) { + v.self_size = self; + fill_info(st, wanted, &v, fd); + v.kind = kind; + v.present &= (wanted | KAL_INFO_KIND); + } + } + if (self >= sizeof v) { *info = v; info->self_size = self; } + else okl::copy(reinterpret_cast(info), reinterpret_cast(&v), self); + return kal_ok; +} + +// A HANDLE WITH LESS (0.23, rights.h). +int kal_fs_dir_attenuate(kal_dir d, const kal_dir_rights* rights, kal_dir* out) { + const int fd = okl::unpack(d.h); + if (fd < 0 || rights == nullptr || out == nullptr) return kal_err_invalid; + if (rights->self_size < __builtin_offsetof(kal_dir_rights, record)) return kal_err_invalid; + constexpr kal_uintptr known = KAL_DIR_READ_ONLY | KAL_DIR_UNCHANGING | KAL_DIR_RECORDED; + if ((rights->flags & ~known) != 0) return kal_err_invalid; + int record = -1; + if (rights->flags & KAL_DIR_RECORDED) { + if (rights->self_size < sizeof(kal_dir_rights)) return kal_err_invalid; + record = okl::unpack(rights->record.h); + if (record < 0) return kal_err_invalid; + // Opened for writing: the record is appended to, not read. + const okl_long fl = okl::sys(okl::nr_fcntl, record, 3 /* F_GETFL */); + if (okl::failed(fl) || (fl & 3) == okl::o_rdonly) return kal_err_invalid; + } + int nfd = -1; + const int e = okl::rights::attenuate(fd, static_cast(rights->flags), record, &nfd); + if (e != kal_ok) return e; + *out = kal_dir{ okl::pack(nfd) }; + return kal_ok; +} + } diff --git a/src/memory.cpp b/src/memory.cpp index 4173f3b..bb9e0ea 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -94,6 +94,94 @@ void* carve(int c) { return b; } +// --- what the environment can give (0.23) --------------------------------- + +// The whole of a small file of the kernel's, into `b'; its length, or 0. +okl_uptr read_small(const char* path, char* b, okl_uptr cap) { + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(path), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return 0; + okl_uptr n = 0; + while (n + 1 < cap) { + const okl_long r = okl::sys(okl::nr_read, fd, reinterpret_cast(b + n), + static_cast(cap - 1 - n)); + if (r == -okl::e_intr) continue; + if (r <= 0) break; + n += static_cast(r); + } + okl::sys(okl::nr_close, fd); + b[n] = '\0'; + return n; +} + +// The number after `key' in `b' (a line `key: 123 kB'), in bytes when the +// line says kB; false where it is absent. +bool field_bytes(const char* b, const char* key, okl_u64& out) { + for (const char* p = b; *p; ) { + const char* k = key; const char* q = p; + while (*k && *q == *k) { ++q; ++k; } + if (*k == '\0' && *q == ':') { + ++q; while (*q == ' ' || *q == '\t') ++q; + okl_u64 v = 0; bool any = false; + while (*q >= '0' && *q <= '9') { v = v * 10 + static_cast(*q++ - '0'); any = true; } + while (*q == ' ') ++q; + if (q[0] == 'k' && q[1] == 'B') v *= 1024; + out = v; + return any; + } + while (*p && *p != '\n') ++p; + if (*p) ++p; + } + return false; +} + +bool read_meminfo(okl_u64& total, okl_u64& available) { + char b[4096]; + if (read_small("/proc/meminfo", b, sizeof b) == 0) return false; + if (!field_bytes(b, "MemTotal", total)) return false; + if (!field_bytes(b, "MemAvailable", available)) { + okl_u64 free = 0, cached = 0; + field_bytes(b, "MemFree", free); + field_bytes(b, "Cached", cached); + available = free + cached; + } + return true; +} + +// The limit of the cgroup-v2 group this program is in, and its use; false +// where there is none (`max', no unified hierarchy, or it cannot be read). +bool group_limit(okl_u64& limit, okl_u64& used) { + char cg[1024]; + const okl_uptr n = read_small("/proc/self/cgroup", cg, sizeof cg); + if (n == 0) return false; + // The unified hierarchy's line: `0::'. + const char* path = nullptr; + for (const char* p = cg; *p; ) { + if (p[0] == '0' && p[1] == ':' && p[2] == ':') { path = p + 3; break; } + while (*p && *p != '\n') ++p; + if (*p) ++p; + } + if (!path) return false; + char file[1200] = "/sys/fs/cgroup"; + okl_uptr o = 14; + for (const char* p = path; *p && *p != '\n' && o + 20 < sizeof file; ++p) file[o++] = *p; + if (o > 0 && file[o - 1] == '/') --o; + auto value = [&](const char* leaf, okl_u64& out) { + okl_uptr k = o; + for (const char* l = leaf; *l && k + 1 < sizeof file; ++l) file[k++] = *l; + file[k] = '\0'; + char b[64]; + if (read_small(file, b, sizeof b) == 0 || b[0] < '0' || b[0] > '9') return false; + okl_u64 v = 0; + for (const char* q = b; *q >= '0' && *q <= '9'; ++q) v = v * 10 + static_cast(*q - '0'); + out = v; + return true; + }; + if (!value("/memory.max", limit)) return false; + if (!value("/memory.current", used)) used = 0; + return true; +} + } // namespace extern "C" { @@ -180,4 +268,29 @@ kal_uintptr kal_memory_granularity(void) { return page != 0 ? static_cast(page) : kPage; } +// WHAT THE ENVIRONMENT CAN GIVE (0.23): the machine's memory and what the +// kernel estimates it can give without taking any from another program +// (MemTotal, MemAvailable), each bounded by the limit of the control group the +// program is in, and what that group already uses. +int kal_memory_info(kal_memory_status* out) { + if (out == nullptr || out->self_size < 2 * sizeof(kal_u32)) return kal_err_invalid; + const kal_u32 self = out->self_size; + kal_memory_status v{}; + v.self_size = self; + okl_u64 total = 0, available = 0; + if (read_meminfo(total, available)) v.present = KAL_MSTATUS_TOTAL | KAL_MSTATUS_AVAILABLE; + okl_u64 limit = 0, used = 0; + if (group_limit(limit, used)) { + if (limit < total || total == 0) total = limit; + const okl_u64 room = limit > used ? limit - used : 0; + if (room < available || !(v.present & KAL_MSTATUS_AVAILABLE)) available = room; + v.present = KAL_MSTATUS_TOTAL | KAL_MSTATUS_AVAILABLE; + } + v.total_bytes = total; + v.available_bytes = available < total ? available : total; + const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); + return kal_ok; +} + } diff --git a/src/observe.cpp b/src/observe.cpp new file mode 100644 index 0000000..a9b0d32 --- /dev/null +++ b/src/observe.cpp @@ -0,0 +1,632 @@ +// openkal.debug and openkal.prof for a program of this environment (0.23): +// the fault reported by what ends the program, an address named, and samples +// taken (SPEC.md clause 11, entry 56). +// +// EVERYTHING HERE THAT RUNS IN A SIGNAL HANDLER ASKS THE KERNEL DIRECTLY AND +// ALLOCATES NOTHING: a program that faulted may have done so inside an +// allocator, and one being sampled may be anywhere at all. Memory another +// context could have left inconsistent is never read; memory that may not be +// mapped --- a frame pointer of a broken chain --- is read through +// `process_vm_readv', which answers EFAULT where a load would fault again. +#include "sys.h" +#include "rights.h" +#include +#include +#include +#include + +// The return from a handler: this kernel requires the restorer of a handler +// installed without a C library to be named. +#if defined(__x86_64__) +extern "C" void okl_signal_return(); +__asm__(".text\n.globl okl_signal_return\n.type okl_signal_return,@function\nokl_signal_return:\n" + " mov $15, %eax\n syscall\n hlt\n"); +#elif defined(__aarch64__) +extern "C" void okl_signal_return(); +__asm__(".text\n.globl okl_signal_return\n.type okl_signal_return,@function\nokl_signal_return:\n" + " mov x8, #139\n svc #0\n"); +#endif +extern "C" void okl_fault_handler(int signo, void* info, void* uc); +extern "C" void okl_sample_handler(int signo, void* info, void* uc); + +namespace { + +// --- reading memory that may not be there ----------------------------------- +bool peek(okl_uptr at, void* out, okl_uptr n) { + struct iov { void* base; okl_uptr len; }; + iov local{ out, n }; + iov remote{ reinterpret_cast(at), n }; + static okl_long self = 0; + if (self == 0) self = okl::sys(okl::nr_getpid); + const okl_long r = okl::sys(okl::nr_process_vm_readv, self, reinterpret_cast(&local), 1, + reinterpret_cast(&remote), 1, 0); + return r == static_cast(n); +} + +// --- writing without a C library ------------------------------------------ +struct line { + char b[512]; okl_uptr n = 0; + line& s(const char* t) { while (*t && n + 1 < sizeof b) b[n++] = *t++; return *this; } + line& s(const char* t, okl_uptr len) { for (okl_uptr i = 0; i < len && n + 1 < sizeof b; ++i) b[n++] = t[i]; return *this; } + line& hex(okl_uptr v) { + char d[18]; int k = 0; + do { d[k++] = "0123456789abcdef"[v & 15]; v >>= 4; } while (v); + s("0x"); + while (k) { if (n + 1 < sizeof b) b[n++] = d[--k]; else --k; } + return *this; + } + line& dec(okl_uptr v) { + char d[24]; int k = 0; + do { d[k++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (k) { if (n + 1 < sizeof b) b[n++] = d[--k]; else --k; } + return *this; + } + void out(int fd) { okl::write_all(fd, b, n); n = 0; } +}; + +// --- where an address lies: /proc/self/maps ------------------------------------ +// +// The mapping that holds `addr', its start, the offset in its file, and the +// file's name; false for memory no file backs. +struct where_t { okl_uptr start = 0, offset = 0; char file[512]; okl_uptr flen = 0; }; + +okl_uptr parse_hex(const char*& p) { + okl_uptr v = 0; + for (;;) { + const char c = *p; + if (c >= '0' && c <= '9') v = v * 16 + static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') v = v * 16 + static_cast(c - 'a' + 10); + else break; + ++p; + } + return v; +} + +bool locate(okl_uptr addr, where_t& w) { + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast("/proc/self/maps"), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return false; + char buf[4096]; + char ln[1024]; + okl_uptr ll = 0; + bool found = false; + for (;;) { + const okl_long r = okl::sys(okl::nr_read, fd, reinterpret_cast(buf), sizeof buf); + if (r == -okl::e_intr) continue; + if (r <= 0) break; + for (okl_long i = 0; i < r && !found; ++i) { + if (buf[i] != '\n') { if (ll + 1 < sizeof ln) ln[ll++] = buf[i]; continue; } + ln[ll] = '\0'; + const char* p = ln; + const okl_uptr lo = parse_hex(p); + if (*p == '-') ++p; + const okl_uptr hi = parse_hex(p); + if (addr >= lo && addr < hi) { + // perms offset dev inode path + while (*p == ' ') ++p; while (*p && *p != ' ') ++p; while (*p == ' ') ++p; + const okl_uptr off = parse_hex(p); + while (*p == ' ') ++p; while (*p && *p != ' ') ++p; while (*p == ' ') ++p; + while (*p && *p != ' ') ++p; while (*p == ' ') ++p; + w.start = lo; w.offset = off; w.flen = 0; + if (*p == '/') { while (*p && w.flen + 1 < sizeof w.file) w.file[w.flen++] = *p++; } + w.file[w.flen] = '\0'; + found = w.flen != 0; + if (!found) break; + } + ll = 0; + } + if (found) break; + } + okl::sys(okl::nr_close, fd); + return found; +} + +// --- the symbol that covers an offset of an ELF file -------------------------- +struct ehdr64 { unsigned char ident[16]; okl_u32 tv; okl_u32 version; okl_u64 entry, phoff, shoff; + okl_u32 flags; unsigned short ehsize, phentsize, phnum, shentsize, shnum, shstrndx; }; +struct phdr64 { okl_u32 type, flags; okl_u64 offset, vaddr, paddr, filesz, memsz, align; }; +struct shdr64 { okl_u32 name, type; okl_u64 flags, addr, offset, size; okl_u32 link, info; okl_u64 align, entsize; }; +struct sym64 { okl_u32 name; unsigned char info, other; unsigned short shndx; okl_u64 value, size; }; + +bool pread_all(int fd, void* out, okl_uptr n, okl_u64 at) { + okl_uptr got = 0; + while (got < n) { + const okl_long r = okl::sys(okl::nr_pread64, fd, reinterpret_cast(static_cast(out) + got), + static_cast(n - got), static_cast(at + got)); + if (r == -okl::e_intr) continue; + if (r <= 0) return false; + got += static_cast(r); + } + return true; +} + +// The name of the symbol of `file' covering file offset `off', into `name', +// and the distance into it; false where the file has no such symbol. +bool symbol_at(const char* file, okl_uptr off, char* name, okl_uptr cap, okl_uptr& len, okl_uptr& delta, + okl_uptr& sym_vaddr, okl_uptr& vaddr_out) { + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(file), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return false; + bool ok = false; + ehdr64 eh{}; + do { + if (!pread_all(static_cast(fd), &eh, sizeof eh, 0)) break; + if (eh.ident[0] != 0x7f || eh.ident[1] != 'E' || eh.ident[4] != 2) break; + // The offset as an address of the file: the loadable segment holding it. + okl_uptr vaddr = ~okl_uptr{0}; + for (unsigned i = 0; i < eh.phnum; ++i) { + phdr64 ph{}; + if (!pread_all(static_cast(fd), &ph, sizeof ph, eh.phoff + i * sizeof ph)) break; + if (ph.type == 1 && off >= ph.offset && off < ph.offset + ph.filesz) { vaddr = off - ph.offset + ph.vaddr; break; } + } + if (vaddr == ~okl_uptr{0}) break; + vaddr_out = vaddr; + // The symbol table, or the dynamic one where the other was removed. + shdr64 table{}, strings{}; + bool have = false; + static const int kinds[2] = { 2 /* SHT_SYMTAB */, 11 /* SHT_DYNSYM */ }; + for (int want : kinds) { + for (unsigned i = 0; i < eh.shnum && !have; ++i) { + shdr64 sh{}; + if (!pread_all(static_cast(fd), &sh, sizeof sh, eh.shoff + i * sizeof sh)) break; + if (static_cast(sh.type) != want) continue; + if (!pread_all(static_cast(fd), &strings, sizeof strings, eh.shoff + sh.link * sizeof sh)) break; + table = sh; have = true; + } + if (have) break; + } + if (!have || table.entsize != sizeof(sym64)) break; + okl_u64 best = 0; okl_u32 best_name = 0; bool any = false; + sym64 chunk[256]; + const okl_u64 count = table.size / sizeof(sym64); + for (okl_u64 at = 0; at < count; at += 256) { + const okl_u64 n = count - at < 256 ? count - at : 256; + if (!pread_all(static_cast(fd), chunk, n * sizeof(sym64), table.offset + at * sizeof(sym64))) break; + for (okl_u64 k = 0; k < n; ++k) { + const sym64& sy = chunk[k]; + if ((sy.info & 15) != 2 /* STT_FUNC */ && (sy.info & 15) != 1 /* STT_OBJECT */) continue; + if (sy.shndx == 0 || sy.value > vaddr) continue; + if (sy.size != 0 && vaddr >= sy.value + sy.size) continue; + if (!any || sy.value > best) { best = sy.value; best_name = sy.name; any = true; } + } + } + if (!any) break; + okl_uptr n = 0; + char c = 1; + while (n + 1 < cap && pread_all(static_cast(fd), &c, 1, strings.offset + best_name + n) && c) name[n++] = c; + name[n] = '\0'; + len = n; delta = vaddr - best; sym_vaddr = best; + ok = n != 0; + } while (false); + okl::sys(okl::nr_close, fd); + return ok; +} + +// --- the fault report ------------------------------------------------------------ +struct frame_regs { okl_uptr pc, fp, sp; }; + +frame_regs from_context(void* uc) { + frame_regs r{}; + auto* b = static_cast(uc); +#if defined(__x86_64__) + // struct ucontext: flags, link, stack (24), then mcontext's gregs; RIP is + // gregs[16], RBP gregs[10], RSP gregs[15]. + okl::copy(&r.pc, b + 40 + 16 * 8, 8); + okl::copy(&r.fp, b + 40 + 10 * 8, 8); + okl::copy(&r.sp, b + 40 + 15 * 8, 8); +#elif defined(__aarch64__) + // flags, link, stack (24), the signal mask with its reserve (128), then + // the sigcontext at a sixteen-byte boundary: fault address, x0..x30, sp, pc. + constexpr okl_uptr mc = 176; + okl::copy(&r.fp, b + mc + 8 + 29 * 8, 8); + okl::copy(&r.sp, b + mc + 8 + 31 * 8, 8); + okl::copy(&r.pc, b + mc + 8 + 32 * 8, 8); +#endif + return r; +} + +// The return addresses of a chain of frame pointers. +int walk(frame_regs r, okl_uptr* pcs, int max) { + int n = 0; + if (r.pc && n < max) pcs[n++] = r.pc; + okl_uptr fp = r.fp; + for (int i = 0; i < 256 && n < max; ++i) { + if (fp == 0 || (fp & 7) != 0 || fp < r.sp) break; + okl_uptr pair[2]; + if (!peek(fp, pair, sizeof pair)) break; + if (pair[1] == 0) break; + pcs[n++] = pair[1]; + if (pair[0] <= fp) break; // a chain runs toward older frames + fp = pair[0]; + } + return n; +} + +void describe_frame(line& l, okl_uptr pc) { + where_t w; + if (!locate(pc, w)) return; + const okl_uptr off = pc - w.start + w.offset; + l.s(" ").s(w.file, w.flen).s("+").hex(off); + char name[256]; okl_uptr nlen = 0, delta = 0, sv = 0, va = 0; + if (symbol_at(w.file, off, name, sizeof name, nlen, delta, sv, va)) + l.s(" ").s(name, nlen).s("+").hex(delta); +} + +const char* fault_name(int signo, int code) { + switch (signo) { + case 11: return code == 2 ? "an access the program may not make (SIGSEGV, protection)" + : "an access to memory that is not there (SIGSEGV)"; + case 7: return "an access the memory cannot serve (SIGBUS)"; + case 4: return "an instruction the program may not execute (SIGILL)"; + case 8: return "an arithmetic fault (SIGFPE)"; + default: return "a fault"; + } +} + +struct siginfo_head { int signo, errnum, code, pad; okl_uptr addr; }; + +void report(int signo, void* info, void* uc) { + const auto* si = static_cast(info); + line l; + l.s("openkal: fault: ").s(fault_name(signo, si ? si->code : 0)).s(" at ").hex(si ? si->addr : 0).s("\n"); + l.out(2); + okl_uptr pcs[64]; + const int n = walk(from_context(uc), pcs, 64); + for (int i = 0; i < n; ++i) { + l.s("openkal: #").dec(static_cast(i)).s(" ").hex(pcs[i]); + describe_frame(l, i == 0 ? pcs[i] : pcs[i] - 1); + l.s("\n"); + l.out(2); + } +} + + + +struct k_sigaction { void* handler; unsigned long flags; void* restorer; unsigned long mask; }; +constexpr unsigned long sa_siginfo = 4, sa_onstack = 0x08000000, sa_restorer = 0x04000000, + sa_resethand = 0x80000000, sa_restart = 0x10000000; + +alignas(16) unsigned char g_alt_stack[256 * 1024]; + +// INSTALLED AT INCEPTION, for every program of this implementation, on a stack +// of its own so that a program that exhausted its stack is still reported. +// Having reported, the handler gives the signal its default disposition again, +// so the fault repeats and the program ends as the kernel ends it, its core +// kept. It is not reset on entry (SA_RESETHAND): a fault that is a touch being +// recorded (below) is the handler's to resolve, and the program continues. +[[gnu::constructor(103)]] void install_fault_report() { + struct { void* sp; int flags; okl_uptr size; } ss{ g_alt_stack, 0, sizeof g_alt_stack }; + okl::sys(okl::nr_sigaltstack, reinterpret_cast(&ss), 0); + static const int faults[4] = { 11, 7, 4, 8 }; + for (int signo : faults) { + k_sigaction old{}; + if (okl::failed(okl::sys(okl::nr_rt_sigaction, signo, 0, reinterpret_cast(&old), sizeof old.mask))) + continue; + // A disposition the program was started with, other than the default, + // is the program's to keep. + if (old.handler != nullptr) continue; + k_sigaction act{ reinterpret_cast(&okl_fault_handler), + sa_siginfo | sa_onstack | sa_restorer, + reinterpret_cast(&okl_signal_return), 0 }; + okl::sys(okl::nr_rt_sigaction, signo, reinterpret_cast(&act), 0, sizeof act.mask); + } +} + +// --- touches (0.23) ------------------------------------------------------------------ +// +// A part being recorded is made readable and not executable: its first +// execution of a page is a fault the handler resolves --- the page made +// executable again, its address recorded once --- and the instruction is +// carried out. Only executing faults: a read of the part, the kernel's +// included, proceeds as it did. +struct touched_part { + okl_uptr from = 0, to = 0; + int prot = 0; + volatile unsigned char* seen = nullptr; // a bit for each page + okl_uptr seen_bytes = 0; +}; +constexpr int kTouchedParts = 16; +touched_part g_parts[kTouchedParts]; +volatile int g_part_count = 0; +volatile int g_touching = 0; +okl_uptr g_touch_page = 4096; + +struct touch_ring { + okl_uptr* pages = nullptr; + volatile okl_uptr* ready = nullptr; + okl_uptr cap = 0; + volatile okl_uptr written = 0, taken = 0; +}; +touch_ring g_touches; + +// The disposition of SIGSEGV before touches were recorded, where it was not +// this handler's: a fault that is not a touch is the program's. +k_sigaction g_segv_before{}; +bool g_segv_replaced = false; + +// The protection of the pages of [from, to) from /proc/self/maps: one for all +// of them, or -1. +int protection_of(okl_uptr from, okl_uptr to) { + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast("/proc/self/maps"), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return -1; + char buf[4096], ln[1024]; + okl_uptr ll = 0, covered = from; + int prot = -2; + bool done = false; + for (;;) { + const okl_long r = okl::sys(okl::nr_read, fd, reinterpret_cast(buf), sizeof buf); + if (r == -okl::e_intr) continue; + if (r <= 0) break; + for (okl_long i = 0; i < r && !done; ++i) { + if (buf[i] != '\n') { if (ll + 1 < sizeof ln) ln[ll++] = buf[i]; continue; } + ln[ll] = '\0'; + ll = 0; + const char* p = ln; + const okl_uptr lo = parse_hex(p); + if (*p == '-') ++p; + const okl_uptr hi = parse_hex(p); + if (hi <= covered || lo >= to) continue; + if (lo > covered) { prot = -1; done = true; break; } // a hole + while (*p == ' ') ++p; + const int here = (p[0] == 'r' ? okl::prot_read : 0) | (p[1] == 'w' ? okl::prot_write : 0) + | (p[2] == 'x' ? okl::prot_exec : 0); + if (prot == -2) prot = here; else if (prot != here) { prot = -1; done = true; break; } + covered = hi; + if (covered >= to) { done = true; break; } + } + if (done) break; + } + okl::sys(okl::nr_close, fd); + return covered >= to ? prot : -1; +} + +// A touch, from the handler: true when the fault was one and is resolved. +bool touched(int signo, const siginfo_head* si) { + if (signo != 11 || !__atomic_load_n(&g_touching, __ATOMIC_ACQUIRE) || si == nullptr) return false; + const okl_uptr addr = si->addr; + const int n = __atomic_load_n(&g_part_count, __ATOMIC_ACQUIRE); + for (int k = 0; k < n; ++k) { + touched_part& t = g_parts[k]; + if (addr < t.from || addr >= t.to) continue; + const okl_uptr page = addr & ~(g_touch_page - 1); + const okl_uptr index = (page - t.from) / g_touch_page; + const unsigned char bit = static_cast(1u << (index & 7)); + const unsigned char was = __atomic_fetch_or(&t.seen[index >> 3], bit, __ATOMIC_ACQ_REL); + // The page executable again before the instruction is retried; a + // second context that met it at once finds the bit and only retries. + okl::sys(okl::nr_mprotect, static_cast(page), static_cast(g_touch_page), t.prot); + if (!(was & bit) && g_touches.pages) { + const okl_uptr mine = __atomic_fetch_add(&g_touches.written, 1, __ATOMIC_ACQ_REL); + if (mine - __atomic_load_n(&g_touches.taken, __ATOMIC_ACQUIRE) < g_touches.cap) { + g_touches.pages[mine % g_touches.cap] = page; + __atomic_store_n(&g_touches.ready[mine % g_touches.cap], mine + 1, __ATOMIC_RELEASE); + } + } + return true; + } + return false; +} + +void fault(int signo, void* info, void* uc) { + if (touched(signo, static_cast(info))) return; + // A fault that is not a touch, met while SIGSEGV was the program's: the + // program's handler, as the program installed it. + if (signo == 11 && g_segv_replaced && g_segv_before.handler != nullptr + && g_segv_before.handler != reinterpret_cast(1)) { + if (g_segv_before.flags & sa_siginfo) + reinterpret_cast(g_segv_before.handler)(signo, info, uc); + else + reinterpret_cast(g_segv_before.handler)(signo); + return; + } + report(signo, info, uc); + // The default disposition, and the fault repeats as the kernel's. + k_sigaction standard{}; + okl::sys(okl::nr_rt_sigaction, signo, reinterpret_cast(&standard), 0, sizeof standard.mask); +} + +// --- samples ----------------------------------------------------------------------- +struct ring { + kal_prof_sample* slots = nullptr; + volatile okl_uptr* ready = nullptr; // the sequence number + 1 of the sample a slot holds + okl_uptr cap = 0; + volatile okl_uptr written = 0; // reserved by the handler + volatile okl_uptr taken = 0; + volatile int on = 0; +}; +ring g_ring; + +okl_u64 monotonic() { + struct { okl_long s, ns; } t{}; + okl::sys(okl::nr_clock_gettime, 1 /* CLOCK_MONOTONIC */, reinterpret_cast(&t)); + return static_cast(t.s) * 1000000000ull + static_cast(t.ns); +} + +void sample(void* uc) { + if (!__atomic_load_n(&g_ring.on, __ATOMIC_ACQUIRE)) return; + // A slot is reserved before it is written; when the ring holds as many as + // it can, the sample is not recorded. + const okl_uptr at = __atomic_load_n(&g_ring.written, __ATOMIC_RELAXED); + if (at - __atomic_load_n(&g_ring.taken, __ATOMIC_ACQUIRE) >= g_ring.cap) return; + const okl_uptr mine = __atomic_fetch_add(&g_ring.written, 1, __ATOMIC_ACQ_REL); + if (mine - __atomic_load_n(&g_ring.taken, __ATOMIC_ACQUIRE) >= g_ring.cap) return; + kal_prof_sample& s = g_ring.slots[mine % g_ring.cap]; + okl_uptr pcs[KAL_PROF_SAMPLE_DEPTH]; + const int n = walk(from_context(uc), pcs, KAL_PROF_SAMPLE_DEPTH); + s.time_ns = monotonic(); + s.context = kal_task_current(); + s.depth = static_cast(n); + s.reserved = 0; + for (int i = 0; i < n; ++i) s.pc[i] = pcs[i]; + __atomic_store_n(&g_ring.ready[mine % g_ring.cap], mine + 1, __ATOMIC_RELEASE); +} + +k_sigaction g_prof_before{}; + +} // namespace + +extern "C" void okl_fault_handler(int signo, void* info, void* uc) { fault(signo, info, uc); } +extern "C" void okl_sample_handler(int, void*, void* uc) { sample(uc); } + +extern "C" { + +kal_uintptr kal_debug_props(void) { return KAL_DEBUG_PROP_FAULT_REPORT; } + +// AN ADDRESS NAMED (0.23 for a program of this environment): the mapping that +// holds it, and the symbol table of its file --- the program's, a library's, +// or an image's, which is the same format. +kal_intptr kal_debug_symbolize(kal_uintptr address, char* out, kal_uintptr cap, kal_uintptr* start) { + if (address == 0) return -static_cast(kal_err_not_found); + where_t w; + if (!locate(address, w)) return -static_cast(kal_err_not_found); + const okl_uptr off = address - w.start + w.offset; + char name[1024]; okl_uptr nlen = 0, delta = 0, sv = 0, va = 0; + if (!symbol_at(w.file, off, name, sizeof name, nlen, delta, sv, va)) + return -static_cast(kal_err_not_found); + if (out && cap) okl::copy(out, name, nlen < cap ? nlen : cap); + if (start) *start = address - delta; + return static_cast(nlen); +} + +// THIS IMPLEMENTATION DOES NOT MEASURE A PROGRAM ITSELF: a loader that binds +// an image does (clause 12.3). So the program is not measured, a counter is +// accepted and never read, and a phase is accepted and recorded nowhere. +int kal_prof_active(void) { return 0; } +int kal_prof_counter(const char*, kal_uintptr, const volatile kal_u64*) { return kal_ok; } +void kal_prof_mark(const char*, kal_uintptr, int) {} + +kal_uintptr kal_prof_props(void) { return KAL_PROF_PROP_SAMPLES | KAL_PROF_PROP_TOUCHES; } + +// SAMPLES: the processor-time timer of the whole program (ITIMER_PROF), whose +// signal the kernel delivers to the context that is using the time, and the +// chain of that context's frames. +int kal_prof_sample_begin(kal_u32 per_second) { + if (per_second == 0 || per_second > 10000) return kal_err_invalid; + if (__atomic_load_n(&g_ring.on, __ATOMIC_ACQUIRE)) return kal_err_again; + if (!g_ring.slots) { + // Address space for a minute of samples at a thousand a second; a page is + // resident once a sample is written to it. + constexpr okl_uptr cap = 65536; + const okl_uptr bytes = cap * sizeof(kal_prof_sample) + cap * sizeof(okl_uptr); + const okl_long m = okl::sys(okl::nr_mmap, 0, static_cast(bytes), + okl::prot_read | okl::prot_write, okl::map_private | okl::map_anonymous, -1, 0); + if (okl::failed(m)) return kal_err_no_memory; + g_ring.slots = reinterpret_cast(m); + g_ring.ready = reinterpret_cast(m + static_cast(cap * sizeof(kal_prof_sample))); + g_ring.cap = cap; + } + k_sigaction act{ reinterpret_cast(&okl_sample_handler), sa_siginfo | sa_restart | sa_restorer, + reinterpret_cast(&okl_signal_return), 0 }; + if (okl::failed(okl::sys(okl::nr_rt_sigaction, 27 /* SIGPROF */, reinterpret_cast(&act), + reinterpret_cast(&g_prof_before), sizeof act.mask))) + return kal_err_not_supported; + __atomic_store_n(&g_ring.on, 1, __ATOMIC_RELEASE); + const okl_long us = static_cast(1000000 / per_second); + struct { okl_long is, ius, vs, vus; } it{ us / 1000000, us % 1000000, us / 1000000, us % 1000000 }; + if (okl::failed(okl::sys(okl::nr_setitimer, 2 /* ITIMER_PROF */, reinterpret_cast(&it), 0))) { + __atomic_store_n(&g_ring.on, 0, __ATOMIC_RELEASE); + okl::sys(okl::nr_rt_sigaction, 27, reinterpret_cast(&g_prof_before), 0, sizeof act.mask); + return kal_err_not_supported; + } + return kal_ok; +} + +kal_intptr kal_prof_sample_take(kal_prof_sample* out, kal_uintptr count) { + if (out == nullptr && count != 0) return -static_cast(kal_err_invalid); + if (!g_ring.slots) return 0; + kal_uintptr n = 0; + while (n < count) { + const okl_uptr t = __atomic_load_n(&g_ring.taken, __ATOMIC_ACQUIRE); + if (__atomic_load_n(&g_ring.ready[t % g_ring.cap], __ATOMIC_ACQUIRE) != t + 1) break; + out[n++] = g_ring.slots[t % g_ring.cap]; + __atomic_store_n(&g_ring.taken, t + 1, __ATOMIC_RELEASE); + } + return static_cast(n); +} + +int kal_prof_sample_end(void) { + if (!__atomic_load_n(&g_ring.on, __ATOMIC_ACQUIRE)) return kal_err_invalid; + struct { okl_long is, ius, vs, vus; } stop{}; + okl::sys(okl::nr_setitimer, 2, reinterpret_cast(&stop), 0); + __atomic_store_n(&g_ring.on, 0, __ATOMIC_RELEASE); + okl::sys(okl::nr_rt_sigaction, 27, reinterpret_cast(&g_prof_before), 0, sizeof g_prof_before.mask); + return kal_ok; +} + +// TOUCHES (0.23). +int kal_prof_touch_begin(const void* at, kal_uintptr size) { + const okl_uptr g = kal_exec_granularity(); + const okl_uptr from = reinterpret_cast(at); + if (size == 0 || (from & (g - 1)) != 0 || (size & (g - 1)) != 0) return kal_err_invalid; + const int prot = protection_of(from, from + size); + if (prot < 0 || !(prot & okl::prot_exec)) return kal_err_invalid; + if (__atomic_load_n(&g_part_count, __ATOMIC_ACQUIRE) == kTouchedParts) return kal_err_no_memory; + g_touch_page = g; + if (!g_touches.pages) { + constexpr okl_uptr cap = 65536; + const okl_long m = okl::sys(okl::nr_mmap, 0, static_cast(cap * 2 * sizeof(okl_uptr)), + okl::prot_read | okl::prot_write, okl::map_private | okl::map_anonymous, -1, 0); + if (okl::failed(m)) return kal_err_no_memory; + g_touches.pages = reinterpret_cast(m); + g_touches.ready = reinterpret_cast(m + static_cast(cap * sizeof(okl_uptr))); + g_touches.cap = cap; + } + const okl_uptr bits = (size / g + 7) / 8; + const okl_long b = okl::sys(okl::nr_mmap, 0, static_cast(bits), okl::prot_read | okl::prot_write, + okl::map_private | okl::map_anonymous, -1, 0); + if (okl::failed(b)) return kal_err_no_memory; + // The handler resolves the touches, whoever holds SIGSEGV. + k_sigaction now{}; + okl::sys(okl::nr_rt_sigaction, 11, 0, reinterpret_cast(&now), sizeof now.mask); + if (now.handler != reinterpret_cast(&okl_fault_handler)) { + g_segv_before = now; + g_segv_replaced = true; + k_sigaction act{ reinterpret_cast(&okl_fault_handler), sa_siginfo | sa_onstack | sa_restorer, + reinterpret_cast(&okl_signal_return), 0 }; + okl::sys(okl::nr_rt_sigaction, 11, reinterpret_cast(&act), 0, sizeof act.mask); + } + const int k = __atomic_load_n(&g_part_count, __ATOMIC_ACQUIRE); + g_parts[k] = touched_part{ from, from + size, prot, reinterpret_cast(b), bits }; + __atomic_store_n(&g_part_count, k + 1, __ATOMIC_RELEASE); + __atomic_store_n(&g_touching, 1, __ATOMIC_RELEASE); + const okl_long r = okl::sys(okl::nr_mprotect, static_cast(from), static_cast(size), + prot & ~okl::prot_exec); + if (okl::failed(r)) { + __atomic_store_n(&g_part_count, k, __ATOMIC_RELEASE); + return okl::translate(r); + } + return kal_ok; +} + +kal_intptr kal_prof_touch_take(kal_uintptr* out, kal_uintptr count) { + if (out == nullptr && count != 0) return -static_cast(kal_err_invalid); + if (!g_touches.pages) return 0; + kal_uintptr n = 0; + while (n < count) { + const okl_uptr t = __atomic_load_n(&g_touches.taken, __ATOMIC_ACQUIRE); + if (__atomic_load_n(&g_touches.ready[t % g_touches.cap], __ATOMIC_ACQUIRE) != t + 1) break; + out[n++] = g_touches.pages[t % g_touches.cap]; + __atomic_store_n(&g_touches.taken, t + 1, __ATOMIC_RELEASE); + } + return static_cast(n); +} + +// Every part as it was; the pages' bits stay for the life of the program (a +// context may still be in the handler with them). +int kal_prof_touch_end(void) { + const int n = __atomic_load_n(&g_part_count, __ATOMIC_ACQUIRE); + if (n == 0) return kal_err_invalid; + for (int k = 0; k < n; ++k) + okl::sys(okl::nr_mprotect, static_cast(g_parts[k].from), + static_cast(g_parts[k].to - g_parts[k].from), g_parts[k].prot); + __atomic_store_n(&g_touching, 0, __ATOMIC_RELEASE); + __atomic_store_n(&g_part_count, 0, __ATOMIC_RELEASE); + if (g_segv_replaced) { + okl::sys(okl::nr_rt_sigaction, 11, reinterpret_cast(&g_segv_before), 0, sizeof g_segv_before.mask); + g_segv_replaced = false; + } + return kal_ok; +} + +} // extern "C" diff --git a/src/process.cpp b/src/process.cpp index eef7d0a..de72b54 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -1,5 +1,6 @@ #include "sys.h" #include "handle.h" +#include "rights.h" #include #include #include @@ -13,6 +14,8 @@ int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& void job_forget(okl_long unit); bool child_memory(okl_long pid, bool ended, okl_u64& now, okl_u64& peak); bool child_cpu(okl_long pid, okl_u64& ns); +int resource_preference(); +bool child_faults(okl_long pid, okl_u64& minor, okl_u64& major); } namespace { @@ -23,6 +26,10 @@ bool same_prefix(const char* a, const char* b, okl_uptr n) { } +// The kernel's account of a program's use (getrusage, wait4): the times, the +// peak, and in `rest' the counts that follow, the page faults at 3 and 4. +struct k_rusage { okl_long utime_s, utime_us, stime_s, stime_us, maxrss; okl_long rest[13]; }; + constexpr okl_uptr kMaxEntries = 512; constexpr char kPreopens[] = "KAL_PREOPENS="; @@ -36,6 +43,34 @@ bool names_preopens(const char* s, kal_uintptr n) { return true; } +// THE TWO VARIABLES 0.23 WRITES BESIDE IT: what each grant carries +// (`KAL_GRANT_RIGHTS', read in fs.cpp) and the started program's resource +// preference (`KAL_RESOURCES', read in env.cpp). A value of either the caller +// supplied is left out where this start writes its own. +constexpr char kRights[] = "KAL_GRANT_RIGHTS="; +constexpr okl_uptr kRightsLen = sizeof kRights - 1; +constexpr char kResources[] = "KAL_RESOURCES="; +constexpr okl_uptr kResourcesLen = sizeof kResources - 1; + +bool starts_with(const char* s, kal_uintptr n, const char* key, okl_uptr klen) { + if (n < klen) return false; + for (okl_uptr i = 0; i < klen; ++i) if (s[i] != key[i]) return false; + return true; +} + +// What one start conveys beside the grants' names. +struct conveyance { + // KAL_GRANT_RIGHTS, without its number: `{;,,,,}'. + char rights[4096]; + okl_uptr rights_len = 0; + bool any_rights = false; + // The descriptors of the records, placed after the grants. + okl_long record_src[16]; + kal_uintptr records = 0; + // KAL_RESOURCES's value, or empty to leave what the caller's environment holds. + const char* resources = nullptr; +}; + okl_uptr digits(okl_uptr v) { okl_uptr n = 1; while (v >= 10) { v /= 10; ++n; } @@ -142,13 +177,19 @@ struct vector { // and asking for none (a count of zero) is a value with no entries. char* pid_digits = nullptr; + char* rights_pid_digits = nullptr; + bool build_env(const char** items, const kal_uintptr* lens, kal_uintptr n, - const kal_preopen* grants, kal_uintptr g) { + const kal_preopen* grants, kal_uintptr g, const conveyance& c) { if (n > kMaxEntries) { ok = false; return false; } + auto left_out = [&](const char* s, kal_uintptr l) { + return names_preopens(s, l) || starts_with(s, l, kRights, kRightsLen) + || starts_with(s, l, kResources, kResourcesLen); + }; kal_uintptr kept = 0; okl_uptr total = 0; for (kal_uintptr i = 0; i < n; ++i) { - if (names_preopens(items[i], lens[i])) continue; + if (left_out(items[i], lens[i])) continue; ++kept; total += lens[i] + 1; } okl_uptr extra = 0; @@ -157,14 +198,18 @@ struct vector { for (kal_uintptr i = 0; i < g; ++i) extra += 3 + digits(3 + i) + digits(grants[i].len) + grants[i].len; } - slots_bytes = (kept + (grants ? 1 : 0) + 1) * sizeof(char*); + if (c.any_rights) extra += kRightsLen + kPidDigits + c.rights_len + 1; + const okl_uptr res_len = c.resources ? okl::length(c.resources) : 0; + if (c.resources) extra += kResourcesLen + res_len + 1; + slots_bytes = (kept + (grants ? 1 : 0) + (c.any_rights ? 1 : 0) + (c.resources ? 1 : 0) + 1) + * sizeof(char*); bytes_bytes = total + extra == 0 ? 1 : total + extra; slots = static_cast(kal_alloc(slots_bytes, alignof(char*))); bytes = static_cast(kal_alloc(bytes_bytes, 1)); if (!slots || !bytes) { ok = false; return false; } okl_uptr at = 0, k = 0; for (kal_uintptr i = 0; i < n; ++i) { - if (names_preopens(items[i], lens[i])) continue; + if (left_out(items[i], lens[i])) continue; okl::copy(bytes + at, items[i], lens[i]); bytes[at + lens[i]] = '\0'; slots[k++] = bytes + at; @@ -183,6 +228,25 @@ struct vector { okl::copy(o, grants[i].name, grants[i].len); o += grants[i].len; } *o = '\0'; + at = static_cast(o + 1 - bytes); + } + if (c.any_rights) { + char* o = bytes + at; + slots[k++] = o; + okl::copy(o, kRights, kRightsLen); o += kRightsLen; + rights_pid_digits = o; + okl::fill(o, '0', kPidDigits); o += kPidDigits; + okl::copy(o, c.rights, c.rights_len); o += c.rights_len; + *o = '\0'; + at = static_cast(o + 1 - bytes); + } + if (c.resources) { + char* o = bytes + at; + slots[k++] = o; + okl::copy(o, kResources, kResourcesLen); o += kResourcesLen; + okl::copy(o, c.resources, res_len); o += res_len; + *o = '\0'; + at = static_cast(o + 1 - bytes); } slots[k] = nullptr; return true; @@ -190,10 +254,14 @@ struct vector { // In the duplicate, which is the first point at which the number is known. void stamp(okl_long pid) const { - if (!pid_digits) return; - for (int i = static_cast(kPidDigits) - 1; i >= 0; --i) { - pid_digits[i] = static_cast('0' + pid % 10); - pid /= 10; + char* const where[2] = { pid_digits, rights_pid_digits }; + for (char* d : where) { + if (!d) continue; + okl_long v = pid; + for (int i = static_cast(kPidDigits) - 1; i >= 0; --i) { + d[i] = static_cast('0' + v % 10); + v /= 10; + } } } @@ -461,6 +529,71 @@ namespace { // this program's but the digits of its own number in its own environment, // which this program releases unread; the descriptors it moves are its own // copies. +// --- confinement (0.23): Landlock -------------------------------------------- +// +// THE KERNEL HOLDS A CONFINED PROGRAM TO ITS GRANTS, so that one that reaches +// files without this interface --- or has been made to --- reaches nothing +// 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. +int landlock_abi() { + static int abi = -2; + if (abi == -2) { + const okl_long r = okl::sys(okl::nr_landlock_create_ruleset, 0, 0, 1 /* _VERSION */); + abi = okl::failed(r) ? 0 : static_cast(r); + } + return abi; +} + +okl_u64 handled_for(int abi) { + okl_u64 h = (okl_u64{1} << 13) - 1; // EXECUTE ... MAKE_SYM (ABI 1) + if (abi >= 2) h |= okl_u64{1} << 13; // REFER + if (abi >= 3) h |= okl_u64{1} << 14; // TRUNCATE + if (abi >= 5) h |= okl_u64{1} << 15; // IOCTL_DEV + return h; +} + +// In the started image, after the grants are placed and before it is +// 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; + 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); + if (okl::failed(rs)) return rs; + struct __attribute__((packed)) { okl_u64 allowed; int parent; } rule{}; + constexpr okl_u64 exec = 1, read_file = 4, read_dir = 8, write_file = 2; + auto add = [&](okl_u64 allowed, okl_long fd) { + rule.allowed = allowed & attr.fs; + rule.parent = static_cast(fd); + return okl::sys(okl::nr_landlock_add_rule, rs, 1 /* PATH_BENEATH */, reinterpret_cast(&rule), 0); + }; + okl_long why = 0; + for (kal_uintptr i = 0; i < g && why == 0; ++i) { + const bool ro = (rights[i] & okl::rights::read_only) != 0; + const okl_long r = add(ro ? (exec | read_file | read_dir) : attr.fs, static_cast(3 + i)); + if (okl::failed(r)) why = r; + } + auto by_name = [&](okl_long at, const char* name, okl_u64 allowed) { + const okl_long fd = okl::sys(okl::nr_openat, at, reinterpret_cast(name), + okl::o_path | okl::o_cloexec, 0); + if (okl::failed(fd)) return; // absent: nothing to allow + add(allowed, fd); + okl::sys(okl::nr_close, fd); + }; + if (why == 0) { + by_name(base, program, exec | read_file); + by_name(okl::at_fdcwd, "/proc", read_file | read_dir); + by_name(okl::at_fdcwd, "/dev/null", read_file | write_file); + okl::sys(okl::nr_prctl, 38 /* PR_SET_NO_NEW_PRIVS */, 1, 0, 0, 0); + const okl_long r = okl::sys(okl::nr_landlock_restrict_self, rs, 0); + if (okl::failed(r)) why = r; + } + okl::sys(okl::nr_close, rs); + return why; +} + struct start_plan { const kal_spawn* how; const okl::terminated* name; @@ -474,6 +607,9 @@ struct start_plan { bool named, use_name, unit, bind; okl_long join, mine; const unsigned long* mask; + const conveyance* carry; // 0.23: the records placed after the grants + bool confine; // 0.23: held to the grants by Landlock + const okl_u32* grant_rights; }; constexpr okl_uptr kStartStack = 64 * 1024; @@ -523,7 +659,8 @@ int start_image(void* arg) { // including where the source happened to be the position itself --- // which `dup3' refuses, and which used to leave the copy marked to // close, so that the grant never arrived. - const okl_long top = static_cast(3 + how->grant_count); + const conveyance& carry = *plan.carry; + const okl_long top = static_cast(3 + how->grant_count + carry.records); auto lift = [&](okl_long fd) -> okl_long { const okl_long r = okl::sys(okl::nr_fcntl, fd, okl::f_dupfd_cloexec, top); if (okl::failed(r)) { @@ -553,6 +690,8 @@ int start_image(void* arg) { const okl_long sou = (how->flags & KAL_SPAWN_NULL_OUT) ? device : ou != 0 ? lift(ou) : 0; const okl_long ser = (how->flags & KAL_SPAWN_NULL_ERR) ? device : er != 0 ? lift(er) : 0; for (kal_uintptr i = 0; i < how->grant_count; ++i) granted[i] = lift(granted[i]); + okl_long records[16]; + for (kal_uintptr j = 0; j < carry.records; ++j) records[j] = lift(carry.record_src[j]); const okl_long base = lift(b); const okl_long work = lift(w); @@ -561,6 +700,10 @@ int start_image(void* arg) { if (ser != 0) okl::sys(okl::nr_dup3, ser, 2, 0); for (kal_uintptr i = 0; i < how->grant_count; ++i) okl::sys(okl::nr_dup3, granted[i], static_cast(3 + i), 0); + // The records of recorded grants, after the grants (0.23); the started + // program learns where from KAL_GRANT_RIGHTS. + for (kal_uintptr j = 0; j < carry.records; ++j) + okl::sys(okl::nr_dup3, records[j], static_cast(3 + how->grant_count + j), 0); // What the CALLER itself inherited without the flag is not a handle it // granted. A program that starts others inside a sandbox passes every @@ -614,6 +757,18 @@ int start_image(void* arg) { okl::sys(okl::nr_exit_group, 127); } + // CONFINED (0.23): the kernel holds the program to its grants from its + // first instruction. A confinement that could not be made is a start that + // failed, reported as one: a caller that asked for it does not receive a + // program without it. + if (plan.confine) { + if (const okl_long why = confine_to(plan.grant_rights, how->grant_count, static_cast(base), p.buf)) { + report.say(why); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + } + // The caller's mask, which the program is started with, once nothing of // the caller's can be entered here any more (above). okl::sys(okl::nr_rt_sigprocmask, 2 /* SIG_SETMASK */, reinterpret_cast(plan.mask), 0, @@ -751,7 +906,16 @@ int kal_process_spawn(const kal_spawn* how, // that outlives it --- which is the failure the flag exists to remove --- so an // unclaimed position is an error and not a thing to proceed without. constexpr kal_uintptr nulls = KAL_SPAWN_NULL_IN | KAL_SPAWN_NULL_OUT | KAL_SPAWN_NULL_ERR; - if (how->flags & ~(KAL_SPAWN_BOUND_LIFETIME | nulls)) return kal_err_not_supported; + constexpr kal_uintptr preferences = KAL_SPAWN_RESOURCES_BALANCED | KAL_SPAWN_RESOURCES_PERFORMANCE + | KAL_SPAWN_RESOURCES_ECONOMY; + if (how->flags & ~(KAL_SPAWN_BOUND_LIFETIME | nulls | preferences | KAL_SPAWN_CONFINE)) + return kal_err_not_supported; + // ONE PREFERENCE (0.23); and a confinement this kernel cannot make is + // refused, not ignored. + 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; // THE NULL DEVICE BESIDE A STREAM AT ONE POSITION is two answers to one // question (openkal 0.22, clause 7.15). if (streams != nullptr @@ -788,9 +952,44 @@ int kal_process_spawn(const kal_spawn* how, const bool named = absolute_under(b, p.buf, path_len, resolved, sizeof resolved); const bool use_name = by_name() && named; + // WHAT EACH GRANT CARRIES, AND THE PREFERENCE (0.23). + conveyance carry; + okl_u32 grant_rights[max_grants] = {}; + for (kal_uintptr i = 0; i < how->grant_count; ++i) { + okl_u32 flags = 0; int record = -1; const char* prefix = ""; okl_uptr plen = 0; + if (!okl::rights::conveyed(static_cast(granted[i]), &flags, &record, &prefix, &plen)) continue; + grant_rights[i] = flags; + long dest = -1; + if (record >= 0 && carry.records < 16) { + dest = static_cast(3 + how->grant_count + carry.records); + carry.record_src[carry.records++] = record; + } + char entry[64]; + char* o = entry; + *o++ = ';'; o = put_decimal(o, i); + *o++ = ','; o = put_decimal(o, flags); + *o++ = ','; + if (dest < 0) { *o++ = '-'; *o++ = '1'; } else o = put_decimal(o, static_cast(dest)); + *o++ = ','; o = put_decimal(o, plen); + *o++ = ','; + const okl_uptr head = static_cast(o - entry); + if (carry.rights_len + head + plen >= sizeof carry.rights) return kal_err_invalid; + okl::copy(carry.rights + carry.rights_len, entry, head); carry.rights_len += head; + okl::copy(carry.rights + carry.rights_len, prefix, plen); carry.rights_len += plen; + carry.any_rights = true; + } + { + const int own = okl::resource_preference(); + const int asked = preference == KAL_SPAWN_RESOURCES_PERFORMANCE ? 1 + : preference == KAL_SPAWN_RESOURCES_ECONOMY ? 2 + : preference == KAL_SPAWN_RESOURCES_BALANCED ? 0 : own; + carry.resources = asked == 1 ? "performance" : asked == 2 ? "economy" + : preference != 0 ? "balanced" : nullptr; + } + vector args, envs; if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; - if (!envs.build_env(envp, envp_lens, envc, how->grants, how->grant_count)) + if (!envs.build_env(envp, envp_lens, envc, how->grants, how->grant_count, carry)) return kal_err_no_memory; const okl_long in = streams ? static_cast(streams->in.h) : 0; @@ -810,7 +1009,7 @@ int kal_process_spawn(const kal_spawn* how, // The bound is 3 + grant_count, because the placements below reach that far. exec_report report; - report.open(how->grant_count); + report.open(how->grant_count + carry.records); // THE STARTED IMAGE SHARES THIS PROGRAM'S MEMORY UNTIL IT IS REPLACED // (CLONE_VM | CLONE_VFORK), on a stack of its own, and this context waits @@ -830,7 +1029,8 @@ int kal_process_spawn(const kal_spawn* how, okl::sys(okl::nr_rt_sigprocmask, 2 /* SIG_SETMASK */, reinterpret_cast(&every), reinterpret_cast(&caller_mask), sizeof every); const start_plan plan{ how, &p, granted, b, w, in, ou, er, &args, &envs, &report, resolved, - named, use_name, unit, bind, join, mine, &caller_mask }; + named, use_name, unit, bind, join, mine, &caller_mask, + &carry, confine, grant_rights }; const okl_long stack = okl::sys(okl::nr_mmap, 0, static_cast(kStartStack), okl::prot_read | okl::prot_write, okl::map_private | okl::map_anonymous, -1, 0); @@ -1337,13 +1537,38 @@ int kal_process_info(kal_process p, kal_u32 wanted, kal_program_info* out) { // MEMORY (openkal 0.20): resident now and at most, from the kernel's record; // of a started program that has ended, its peak from the record it keeps // until it is collected. Only where the structure has room for the fields. - if ((wanted & KAL_PINFO_MEMORY) && self >= sizeof v) { + // Where the caller's structure reaches the fields: one built for 0.22 ends + // with them, and is answered as it was. + constexpr kal_u32 memory_end = static_cast(__builtin_offsetof(kal_program_info, memory_peak_bytes) + 8); + constexpr kal_u32 faults_end = static_cast(__builtin_offsetof(kal_program_info, faults_major) + 8); + if ((wanted & KAL_PINFO_MEMORY) && self >= memory_end) { okl_u64 now = 0, peak = 0; const bool ok = me ? okl::child_memory(0, false, now, peak) : handle_kind(p) == kind_child ? okl::child_memory(pid, ended, now, peak) : (!ended && okl::child_memory(pid, false, now, peak)); if (ok) { v.memory_bytes = now; v.memory_peak_bytes = peak; v.present |= KAL_PINFO_MEMORY; } } + // PAGE FAULTS (0.23): this program's from the kernel's account of it, a + // running one's from its record in /proc, an ended one's from the record + // kept until it is collected. + if ((wanted & (KAL_PINFO_FAULTS | KAL_PINFO_MAJOR)) && self >= faults_end) { + okl_u64 minor = 0, major = 0; + bool ok = false; + if (me) { + k_rusage ru{}; + ok = !okl::failed(okl::sys(okl::nr_getrusage, 0 /* RUSAGE_SELF */, reinterpret_cast(&ru))); + minor = static_cast(ru.rest[3]); major = static_cast(ru.rest[4]); + } else if (!ended) { + char b[1024]; + const okl_long len = read_stat(pid, b); + bool a = false, c = false; + if (len > 0) { minor = stat_number(b, len, 10, a); major = stat_number(b, len, 12, c); } + ok = a && c; + } else if (handle_kind(p) == kind_child) { + ok = okl::child_faults(pid, minor, major); + } + if (ok) { v.faults_minor = minor; v.faults_major = major; v.present |= KAL_PINFO_FAULTS | KAL_PINFO_MAJOR; } + } const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); return kal_ok; @@ -1383,6 +1608,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) | (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 @@ -1484,7 +1710,6 @@ bool resident(okl_long pid, okl_u64& now, okl_u64& peak) { } // An ended program not yet collected: its record, read without collecting it. -struct k_rusage { okl_long utime_s, utime_us, stime_s, stime_us, maxrss; okl_long rest[13]; }; bool ended_usage(okl_long pid, k_rusage& ru) { alignas(8) unsigned char info[128] = {}; const okl_long r = okl::sys(okl::nr_waitid, 1 /* P_PID */, pid, reinterpret_cast(info), @@ -1632,6 +1857,15 @@ bool child_memory(okl_long pid, bool ended, okl_u64& now, okl_u64& peak) { return true; } +// The page faults of an ended program not yet collected (0.23). +bool child_faults(okl_long pid, okl_u64& minor, okl_u64& major) { + k_rusage ru{}; + if (!ended_usage(pid, ru)) return false; + minor = static_cast(ru.rest[3]); + major = static_cast(ru.rest[4]); + return true; +} + bool child_cpu(okl_long pid, okl_u64& ns) { k_rusage ru{}; if (!ended_usage(pid, ru)) return false; diff --git a/src/rights.cpp b/src/rights.cpp new file mode 100644 index 0000000..5bd421c --- /dev/null +++ b/src/rights.cpp @@ -0,0 +1,484 @@ +#include "rights.h" +#include "handle.h" +#include + +namespace okl { int resource_preference(); } + +namespace okl::rights { + +volatile bool g_any = false; + +namespace { + +// --- the lock ---------------------------------------------------------------- +// +// One lock for the table, the records and the caches. Each holder does a little +// work and leaves, except a listing built for a tree that does not change, +// which happens once for each directory listed. +struct spin { + volatile int held = 0; + void lock() { while (__atomic_exchange_n(&held, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); } + void unlock() { __atomic_store_n(&held, 0, __ATOMIC_RELEASE); } +}; +spin g_lock; +struct held { held() { g_lock.lock(); } ~held() { g_lock.unlock(); } }; + +okl_u64 hash(const char* s, okl_uptr n, okl_u64 h = 1469598103934665603ull) { + for (okl_uptr i = 0; i < n; ++i) { h ^= static_cast(s[i]); h *= 1099511628211ull; } + return h; +} + +void* grab(okl_uptr n) { return kal_alloc(n, 16); } +void give_back(void* p, okl_uptr n) { if (p) kal_free(p, n, 16); } + +// --- a set of 64-bit keys, with a byte beside each ----------------------------- +struct keyset { + okl_u64* keys = nullptr; // zero is never a key: hash() of anything is made non-zero + unsigned char* vals = nullptr; + okl_uptr cap = 0, used = 0; + + bool grow() { + const okl_uptr ncap = cap ? cap * 2 : 256; + auto* nk = static_cast(grab(ncap * sizeof(okl_u64))); + auto* nv = static_cast(grab(ncap)); + if (!nk || !nv) { give_back(nk, ncap * sizeof(okl_u64)); give_back(nv, ncap); return false; } + okl::fill(nk, 0, ncap * sizeof(okl_u64)); + for (okl_uptr i = 0; i < cap; ++i) { + if (keys[i] == 0) continue; + okl_uptr j = keys[i] & (ncap - 1); + while (nk[j] != 0) j = (j + 1) & (ncap - 1); + nk[j] = keys[i]; nv[j] = vals[i]; + } + give_back(keys, cap * sizeof(okl_u64)); give_back(vals, cap); + keys = nk; vals = nv; cap = ncap; + return true; + } + // Inserts; false when it was there already (or could not be added). + bool add(okl_u64 k, unsigned char v) { + if (k == 0) k = 1; + if ((used + 1) * 2 > cap && !grow()) return false; + okl_uptr j = k & (cap - 1); + while (keys[j] != 0) { if (keys[j] == k) return false; j = (j + 1) & (cap - 1); } + keys[j] = k; vals[j] = v; ++used; + return true; + } + // The byte beside `k', or -1. + int find(okl_u64 k) const { + if (k == 0) k = 1; + if (cap == 0) return -1; + okl_uptr j = k & (cap - 1); + while (keys[j] != 0) { if (keys[j] == k) return vals[j]; j = (j + 1) & (cap - 1); } + return -1; + } + void clear() { + give_back(keys, cap * sizeof(okl_u64)); give_back(vals, cap); + keys = nullptr; vals = nullptr; cap = used = 0; + } +}; + +// --- the trees --------------------------------------------------------------- +// +// A ROOT is one attenuation: the directory it was made of, its rights, its +// record and what it has listed. Every handle derived from it names the root +// and the path beneath it. +constexpr int kRoots = 64; +constexpr okl_uptr kBatch = 64 * 1024; + +struct root { + bool live = false; + int refs = 0; + okl_u32 flags = 0; + int base = -1; // a descriptor of the root, to list beneath + // The record. + int record = -1; // opened to append + char* rprefix = nullptr; // where this root lies beneath the directory the record is of + okl_uptr rplen = 0; + keyset seen; // kind and name, once each + char* batch = nullptr; + okl_uptr batched = 0; + bool wrote = false; + // What was listed, for a tree that does not change: each directory listed, + // keyed by its path, and the names in it. + keyset listed; // directory path -> 1 + keyset names; // (directory path, name) -> kind + okl_uptr budget = 0; +}; +root g_roots[kRoots]; + +// A HANDLE: its root and the path of its directory beneath the root. +struct entry { + int fd = -1; + int root = -1; + okl_u32 flags = 0; + char* prefix = nullptr; + okl_uptr plen = 0; +}; +constexpr int kEntries = 4096; +entry g_entries[kEntries]; +unsigned short g_slot[okl::kMaxDescriptor]; // entry index + 1; zero for none + +entry* entry_of(int fd) { + if (fd < 0 || fd >= okl::kMaxDescriptor || g_slot[fd] == 0) return nullptr; + return &g_entries[g_slot[fd] - 1]; +} + +entry* new_entry(int fd) { + if (fd < 0 || fd >= okl::kMaxDescriptor) return nullptr; + for (int i = 0; i < kEntries; ++i) + if (g_entries[i].fd < 0) { g_entries[i].fd = fd; g_slot[fd] = static_cast(i + 1); return &g_entries[i]; } + return nullptr; +} + +void drop_entry(entry* e) { + if (!e) return; + if (e->fd >= 0 && e->fd < okl::kMaxDescriptor) g_slot[e->fd] = 0; + give_back(e->prefix, e->plen + 1); + *e = entry{}; +} + +int new_root() { + for (int i = 0; i < kRoots; ++i) if (!g_roots[i].live) { g_roots[i] = root{}; g_roots[i].live = true; return i; } + return -1; +} + +okl_uptr budget_for_preference() { + switch (okl::resource_preference()) { + case 1: return okl_uptr{1} << 20; // performance + case 2: return 0; // economy: nothing kept + default: return okl_uptr{1} << 16; // balanced + } +} + +// The path of `name' beneath the root of `e' --- after `lead', where given --- +// into `out'; its length, or 0 when it does not fit. "." components are +// dropped. +okl_uptr full_path(const entry& e, const char* name, okl_uptr len, char* out, okl_uptr cap, + const char* lead = nullptr, okl_uptr lead_len = 0) { + okl_uptr o = 0; + auto put = [&](const char* s, okl_uptr n) { + if (n == 0 || (n == 1 && s[0] == '.')) return true; + if (o + n + 1 >= cap) return false; + if (o) out[o++] = '/'; + okl::copy(out + o, s, n); o += n; + return true; + }; + okl_uptr start = 0; + for (okl_uptr i = 0; lead && i <= lead_len; ++i) + if (i == lead_len || lead[i] == '/') { if (!put(lead + start, i - start)) return 0; start = i + 1; } + start = 0; + for (okl_uptr i = 0; i <= e.plen; ++i) + if (i == e.plen || e.prefix[i] == '/') { if (!put(e.prefix + start, i - start)) return 0; start = i + 1; } + start = 0; + for (okl_uptr i = 0; i <= len; ++i) + if (i == len || name[i] == '/') { if (!put(name + start, i - start)) return 0; start = i + 1; } + out[o] = '\0'; + return o == 0 ? 1 : o; // the root itself: "" written as one byte below +} + +// --- the record ---------------------------------------------------------------- +void flush(root& r) { + if (r.record < 0 || r.batched == 0) return; + okl_uptr at = 0; + while (at < r.batched) { + const okl_long w = okl::sys(okl::nr_write, r.record, reinterpret_cast(r.batch + at), + static_cast(r.batched - at)); + if (w == -okl::e_intr) continue; + if (okl::failed(w) || w == 0) break; + at += static_cast(w); + } + r.batched = 0; +} + +void append(root& r, char kind, const char* path, okl_uptr n) { + if (r.record < 0) return; + if (!r.batch) { r.batch = static_cast(grab(kBatch)); if (!r.batch) return; } + if (n + 2 > kBatch) return; + if (r.batched + n + 2 > kBatch) flush(r); + r.batch[r.batched++] = kind; + okl::copy(r.batch + r.batched, path, n); r.batched += n; + r.batch[r.batched++] = '\0'; + r.wrote = true; +} + +// --- the listings of a tree that does not change --------------------------------- +okl_u64 name_key(okl_u64 dir, const char* s, okl_uptr n) { return hash(s, n, dir ^ 0x9e3779b97f4a7c15ull); } + +// Lists `dir' (a path beneath the root, "" for the root) into the root's names; +// false when it cannot. +bool list(root& r, const char* dir, okl_uptr dlen, okl_u64 key) { + if (r.listed.find(key) >= 0) return true; + if (r.budget == 0) return false; + char path[4096]; + if (dlen + 1 >= sizeof path) return false; + if (dlen == 0) { path[0] = '.'; path[1] = '\0'; } + else { okl::copy(path, dir, dlen); path[dlen] = '\0'; } + const okl_long fd = okl::sys(okl::nr_openat, r.base, reinterpret_cast(path), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + if (okl::failed(fd)) return false; + alignas(8) char buf[16384]; + okl_uptr count = 0; + bool ok = true; + for (;;) { + const okl_long got = okl::sys(okl::nr_getdents64, fd, reinterpret_cast(buf), sizeof buf); + if (got == -okl::e_intr) continue; + if (okl::failed(got)) { ok = false; break; } + if (got == 0) break; + for (okl_uptr at = 0; at < static_cast(got);) { + auto* d = reinterpret_cast(buf + at); + at += d->reclen; + const okl_uptr n = okl::length(d->name); + if ((n == 1 && d->name[0] == '.') || (n == 2 && d->name[0] == '.' && d->name[1] == '.')) continue; + if (count >= r.budget) { ok = false; break; } + r.names.add(name_key(key, d->name, n), d->type); + ++count; + } + if (!ok) break; + } + okl::sys(okl::nr_close, fd); + if (!ok) return false; + r.budget -= count; + r.listed.add(key, 1); + return true; +} + +} // namespace + +okl_u32 of(int fd) { + if (!g_any) return 0; + held h; + const entry* e = entry_of(fd); + return e ? e->flags : 0; +} + +int attenuate(int fd, okl_u32 add, int record, int* out) { + const okl_long dup = okl::sys(okl::nr_fcntl, fd, okl::f_dupfd_cloexec, 3); + if (okl::failed(dup)) return okl::translate(dup); + const int nfd = static_cast(dup); + // The record is appended to through a description of its own, so that the + // holder's position in the file is not moved and every write appends. + int rec = -1; + if (add & recorded) { + char proc[32] = "/proc/self/fd/"; + okl_uptr o = 14; + char digits[12]; int n = 0; unsigned v = static_cast(record); + do { digits[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (n) proc[o++] = digits[--n]; + proc[o] = '\0'; + okl_long r = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(proc), + okl::o_wronly | okl::o_append | okl::o_cloexec, 0); + if (okl::failed(r)) r = okl::sys(okl::nr_fcntl, record, okl::f_dupfd_cloexec, 3); + if (okl::failed(r)) { okl::sys(okl::nr_close, nfd); return okl::translate(r); } + rec = static_cast(r); + } + held h; + const entry* from = entry_of(fd); + entry* e = new_entry(nfd); + if (!e) { okl::sys(okl::nr_close, nfd); if (rec >= 0) okl::sys(okl::nr_close, rec); return 4; /* kal_err_no_memory */ } + const okl_u32 inherited = from ? from->flags : 0; + // A NEW ROOT WHERE THERE IS SOMETHING NEW TO RECORD OR TO STATE; otherwise + // the handle is the same tree with fewer rights. + const bool new_tree = from == nullptr || (add & (recorded | unchanging)) != 0; + if (new_tree) { + const int ri = new_root(); + if (ri < 0) { drop_entry(e); okl::sys(okl::nr_close, nfd); if (rec >= 0) okl::sys(okl::nr_close, rec); return 4; } + root& r = g_roots[ri]; + r.flags = inherited | add; + r.refs = 1; + r.base = static_cast(okl::sys(okl::nr_fcntl, nfd, okl::f_dupfd_cloexec, 3)); + r.record = rec; + r.budget = (r.flags & unchanging) ? budget_for_preference() : 0; + // A RECORD ALREADY KEPT FOR THE TREE GOES ON BEING KEPT, and its names + // stay relative to the directory it is of: this root lies at the + // handle's path beneath that directory. + if (from && from->root >= 0 && !(add & recorded) && g_roots[from->root].record >= 0) { + const root& f = g_roots[from->root]; + r.record = static_cast(okl::sys(okl::nr_fcntl, f.record, okl::f_dupfd_cloexec, 3)); + r.flags |= recorded; + char lead[4096]; + entry at = *from; + okl_uptr n = full_path(at, "", 0, lead, sizeof lead, f.rprefix, f.rplen); + if (n == 1 && lead[0] == '\0') n = 0; + r.rprefix = static_cast(grab(n + 1)); + if (r.rprefix) { okl::copy(r.rprefix, lead, n); r.rprefix[n] = '\0'; r.rplen = n; } + } + e->root = ri; + e->prefix = static_cast(grab(1)); if (e->prefix) e->prefix[0] = '\0'; + e->plen = 0; + } else { + e->root = from->root; + if (e->root >= 0) g_roots[e->root].refs++; + e->prefix = static_cast(grab(from->plen + 1)); + if (e->prefix) { okl::copy(e->prefix, from->prefix, from->plen); e->prefix[from->plen] = '\0'; } + e->plen = from->plen; + } + e->flags = inherited | add; + g_any = true; + *out = nfd; + return 0; +} + +bool derive(int parent, int child, const char* name, okl_uptr len) { + if (!g_any) return true; + held h; + const entry* p = entry_of(parent); + if (!p) return true; + entry* e = new_entry(child); + if (!e) return false; + e->flags = p->flags; + e->root = p->root; + if (e->root >= 0) g_roots[e->root].refs++; + char buf[4096]; + entry tmp = *p; + const okl_uptr n = full_path(tmp, name, len, buf, sizeof buf); + const bool root_itself = n == 1 && buf[0] == '\0'; + const okl_uptr plen = root_itself ? 0 : n; + e->prefix = static_cast(grab(plen + 1)); + if (e->prefix) { okl::copy(e->prefix, buf, plen); e->prefix[plen] = '\0'; } + e->plen = e->prefix ? plen : 0; + return true; +} + +void forget(int fd) { + if (!g_any) return; + held h; + entry* e = entry_of(fd); + if (!e) return; + const int ri = e->root; + drop_entry(e); + if (ri >= 0 && --g_roots[ri].refs <= 0) { + // The last handle of the tree: what it recorded is written now. The + // root itself stays until the program ends, which writes its `e'. + flush(g_roots[ri]); + g_roots[ri].names.clear(); + g_roots[ri].listed.clear(); + if (g_roots[ri].base >= 0) { okl::sys(okl::nr_close, g_roots[ri].base); g_roots[ri].base = -1; } + } +} + +void note(int fd, char kind, const char* name, okl_uptr len) { + if (!g_any) return; + held h; + const entry* e = entry_of(fd); + if (!e || e->root < 0 || !(e->flags & recorded)) return; + root& r = g_roots[e->root]; + if (r.record < 0) return; + char buf[4096]; + okl_uptr n = full_path(*e, name, len, buf, sizeof buf, r.rprefix, r.rplen); + if (n == 0) return; + if (n == 1 && buf[0] == '\0') { buf[0] = '.'; buf[1] = '\0'; } + if (!r.seen.add(hash(buf, n, static_cast(static_cast(kind)) * 0x100000001b3ull), 1)) return; + append(r, kind, buf, n); +} + +bool known_absent(int fd, const char* name, okl_uptr len) { + if (!g_any) return false; + held h; + const entry* e = entry_of(fd); + if (!e || e->root < 0 || !(e->flags & unchanging)) return false; + root& r = g_roots[e->root]; + if (r.base < 0) return false; + char path[4096]; + const okl_uptr n = full_path(*e, name, len, path, sizeof path); + if (n == 0 || (n == 1 && path[0] == '\0')) return false; + // Each component in the listing of the directory before it; a component + // that is not a directory, or a directory that cannot be listed, settles + // nothing. + okl_uptr start = 0; + okl_u64 dir = hash("", 0); + for (okl_uptr i = 0; i <= n; ++i) { + if (i < n && path[i] != '/') continue; + if (!list(r, path, start == 0 ? 0 : start - 1, dir)) return false; + const int kind = r.names.find(name_key(dir, path + start, i - start)); + if (kind < 0) return true; + if (i == n) return false; + if (kind != okl::dt_dir) return false; + dir = hash(path, i); + start = i + 1; + } + return false; +} + +void changed(int fd) { + if (!g_any) return; + held h; + const entry* e = entry_of(fd); + if (!e || e->root < 0) return; + root& r = g_roots[e->root]; + r.names.clear(); + r.listed.clear(); + if (r.flags & unchanging) r.budget = budget_for_preference(); +} + +bool conveyed(int fd, okl_u32* flags, int* record, const char** prefix, okl_uptr* prefix_len) { + if (!g_any) return false; + held h; + const entry* e = entry_of(fd); + if (!e) return false; + *flags = e->flags; + *record = (e->root >= 0 && (e->flags & recorded)) ? g_roots[e->root].record : -1; + // Where the granted directory lies beneath the directory the record is + // of, written to a buffer of this table's that lives until the next call. + static char lead[4096]; + okl_uptr n = 0; + if (e->root >= 0) { + const root& r = g_roots[e->root]; + n = full_path(*e, "", 0, lead, sizeof lead, r.rprefix, r.rplen); + if (n == 1 && lead[0] == '\0') n = 0; + } + *prefix = lead; + *prefix_len = n; + return true; +} + +void adopt(int fd, okl_u32 flags, int record, const char* prefix, okl_uptr prefix_len) { + held h; + entry* e = new_entry(fd); + if (!e) return; + const int ri = new_root(); + if (ri < 0) { drop_entry(e); return; } + root& r = g_roots[ri]; + r.flags = flags; + r.refs = 1; + r.base = static_cast(okl::sys(okl::nr_fcntl, fd, okl::f_dupfd_cloexec, 3)); + if (record >= 0) { + // Marked to close on replacement as the grant itself is: the record + // reaches this program and not the programs it starts, unless it grants + // the handle on. + okl::sys(okl::nr_fcntl, record, okl::f_setfd, okl::fd_cloexec); + r.record = record; + } + r.budget = (flags & unchanging) ? budget_for_preference() : 0; + r.rprefix = static_cast(grab(prefix_len + 1)); + if (r.rprefix) { okl::copy(r.rprefix, prefix, prefix_len); r.rprefix[prefix_len] = '\0'; r.rplen = prefix_len; } + e->root = ri; + e->flags = flags; + e->prefix = static_cast(grab(1)); + if (e->prefix) e->prefix[0] = '\0'; + e->plen = 0; + g_any = true; +} + +void end_of_program() { + if (!g_any) return; + static volatile int ended = 0; + if (__atomic_exchange_n(&ended, 1, __ATOMIC_ACQ_REL)) return; + held h; + for (auto& r : g_roots) { + if (!r.live || r.record < 0) continue; + if (!r.batch) r.batch = static_cast(grab(kBatch)); + if (r.batch) { + if (r.batched + 2 > kBatch) flush(r); + r.batch[r.batched++] = 'e'; + r.batch[r.batched++] = '\0'; + } + flush(r); + } +} + +} // namespace okl::rights + +namespace { +// A program whose C library ends it without kal_exit --- one of the +// environment's own --- writes its records as its destructors run. +[[gnu::destructor]] void records_at_exit() { okl::rights::end_of_program(); } +} // namespace diff --git a/src/rights.h b/src/rights.h new file mode 100644 index 0000000..49b4623 --- /dev/null +++ b/src/rights.h @@ -0,0 +1,57 @@ +// A directory handle with less (openkal 0.23, `kal_fs_dir_attenuate', +// SPEC.md clause 7.16): what a handle may not do, what its holder stated, and +// what it records. +// +// A table indexed by descriptor, consulted by every operation of openkal.fs that +// takes a directory. It is empty in a program that attenuates nothing, and the +// one test of `g_any' is then all an operation pays. +#pragma once +#include "sys.h" + +namespace okl::rights { + +inline constexpr okl_u32 read_only = 1u; // KAL_DIR_READ_ONLY +inline constexpr okl_u32 unchanging = 2u; // KAL_DIR_UNCHANGING +inline constexpr okl_u32 recorded = 4u; // KAL_DIR_RECORDED + +extern volatile bool g_any; + +// The rights a directory descriptor carries; zero for one that carries none. +okl_u32 of(int fd); + +// A new descriptor for the directory `fd' names, carrying the rights of `fd' +// and `add'; `record' is the holder's file for `recorded'. Returns a kal_err +// value, and writes the new descriptor to `out' on kal_ok. +int attenuate(int fd, okl_u32 add, int record, int* out); + +// A directory `child' opened beneath `parent' by `name': it carries what the +// parent carries. Returns false when the table has no room, and the caller then +// refuses the open rather than hand out a directory with more than its base. +bool derive(int parent, int child, const char* name, okl_uptr len); + +// The descriptor is being closed. +void forget(int fd); + +// What a name reached beneath `fd' was found to be, for a recorded handle: +// 'r' read, 'w' written, 's' asked about and present, 'a' absent, 'l' listed. +// A name of length zero is the directory itself. +void note(int fd, char kind, const char* name, okl_uptr len); + +// Whether a handle that states nothing changes beneath it knows `name' to be +// absent, from what it has listed --- without asking the kernel. +bool known_absent(int fd, const char* name, okl_uptr len); + +// Something beneath was changed through a handle of this tree: what was listed +// may no longer hold. +void changed(int fd); + +// THE CONVEYANCE TO A STARTED PROGRAM. What a granted descriptor carries, for +// the environment variable a start writes; and, in the started program, a +// preopen received with rights. +bool conveyed(int fd, okl_u32* flags, int* record, const char** prefix, okl_uptr* prefix_len); +void adopt(int fd, okl_u32 flags, int record, const char* prefix, okl_uptr prefix_len); + +// The program is ending: every record is written, and ended with `e'. +void end_of_program(); + +} // namespace okl::rights diff --git a/src/sys.h b/src/sys.h index 4135dd6..d226462 100644 --- a/src/sys.h +++ b/src/sys.h @@ -132,6 +132,13 @@ enum : okl_long { // openkal 0.20: transfers at an offset, a range's backing returned, and // whether a file may be written by this program (a control group's). nr_pread64 = 17, nr_pwrite64 = 18, nr_madvise = 28, nr_faccessat = 269, + // openkal 0.23: a part sealed, a listing with attributes, page faults + // counted, a fault reported, samples taken, a program confined, and what + // memory is available. + nr_mseal = 462, nr_statx = 332, nr_getrusage = 98, nr_sigaltstack = 131, + nr_setitimer = 38, nr_rt_sigreturn = 15, nr_process_vm_readv = 310, + nr_landlock_create_ruleset = 444, nr_landlock_add_rule = 445, + nr_landlock_restrict_self = 446, nr_sysinfo = 99, }; #elif defined(__aarch64__) @@ -230,6 +237,11 @@ enum : okl_long { nr_rt_sigprocmask = 135, // openkal 0.20, as on the other architecture. nr_pread64 = 67, nr_pwrite64 = 68, nr_madvise = 233, nr_faccessat = 48, + // openkal 0.23, as on the other architecture. + nr_mseal = 462, nr_statx = 291, nr_getrusage = 165, nr_sigaltstack = 132, + nr_setitimer = 103, nr_rt_sigreturn = 139, nr_process_vm_readv = 270, + nr_landlock_create_ruleset = 444, nr_landlock_add_rule = 445, + nr_landlock_restrict_self = 446, nr_sysinfo = 179, }; #else diff --git a/src/version.cpp b/src/version.cpp index 9aab1b8..afc6c41 100644 --- a/src/version.cpp +++ b/src/version.cpp @@ -24,7 +24,10 @@ kal_u64 kal_interfaces(void) { | KAL_IFACE_FS | KAL_IFACE_PROCESS | KAL_IFACE_TASK | KAL_IFACE_EXEC | KAL_IFACE_TERMINAL | KAL_IFACE_NET | KAL_IFACE_DATAGRAM | KAL_IFACE_SPACE | KAL_IFACE_TIMEOUT - | KAL_IFACE_MAP | KAL_IFACE_REGION; + | KAL_IFACE_MAP | KAL_IFACE_REGION + // openkal.prof and openkal.debug for a program of this environment + // (0.23): samples and a fault's report (observe.cpp). + | KAL_IFACE_PROF | KAL_IFACE_DEBUG; } } diff --git a/tests/openkal_0_23.cpp b/tests/openkal_0_23.cpp new file mode 100644 index 0000000..244ad56 --- /dev/null +++ b/tests/openkal_0_23.cpp @@ -0,0 +1,156 @@ +// What openkal-linux 0.24 changes beside openkal 0.23, as this kernel does it. +// +// A RECORD ACROSS A START: a program granted a recorded handle appends what it +// reached to its caller's record, and its end. A CONFINED PROGRAM IS HELD BY +// THE KERNEL: one that writes without this interface --- a raw system call --- +// 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 +import openkal.types; +import openkal.env; +import openkal.fs; +import openkal.process; +import openkal.abort; + +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 n[8]; kal_uintptr l = 0; + if (kal_fs_preopen(i, &d, n, sizeof n, &l) == kal_ok && l == 1 && n[0] == '/') return d; + } + return kal_dir{}; +} + +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) { + const kal_dir r = root(); + const char* args[] = { g_self, errand }; + const kal_uintptr lens[] = { std::strlen(g_self), std::strlen(errand) }; + 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, + nullptr, &p) != kal_ok) + return -1; + int status = -1, terminated = 0; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); + return terminated ? -2 : status; +} +} // namespace + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "--read") == 0) { + // The first preopen is the recorded grant: read a file, ask for an + // absent one, and end through kal_exit so the record is ended. + kal_dir d{}; kal_uintptr l = 0; + if (kal_fs_preopen(0, &d, nullptr, 0, &l) != kal_ok) kal_exit(51); + kal_file f{}; + if (kal_fs_open(d, "present.txt", 11, kal::fs::open::read.bits, &f) != kal_ok) kal_exit(52); + kal_fs_close_file(f); + kal_node_info i{}; i.self_size = sizeof i; + kal_fs_info(d, "absent.txt", 10, 0, kal::fs::field::kind, &i); + kal_exit(0); + } + if (argc > 1 && std::strcmp(argv[1], "--write-raw") == 0) { + // Past openkal: the kernel itself is asked to create a file in the + // read-only grant, and in a directory not granted at all. + const int a = static_cast(::syscall(SYS_openat, AT_FDCWD, "/tmp/okl-0.24-raw.tmp", O_CREAT | O_WRONLY, 0600)); + if (a >= 0) { ::unlink("/tmp/okl-0.24-raw.tmp"); return 61; } + const int b = static_cast(::syscall(SYS_openat, AT_FDCWD, "/etc/hostname", O_RDONLY)); + if (b >= 0) return 62; + return 60; + } + if (argc > 1 && std::strcmp(argv[1], "--thp") == 0) + return static_cast(::prctl(PR_GET_THP_DISABLE, 0, 0, 0, 0)) == 1 ? 70 : 71; + + 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; } + g_self[n] = 0; + if (!mkdtemp(g_dir)) { std::printf("FAIL: a directory for the test\n"); return 1; } + char path[256]; + std::snprintf(path, sizeof path, "%s/present.txt", g_dir); + { const int fd = ::open(path, O_CREAT | O_WRONLY, 0600); (void)!::write(fd, "x", 1); ::close(fd); } + + // A record across a start. + kal_dir dir{}; + check(kal_fs_open_dir(root(), g_dir + 1, std::strlen(g_dir + 1), &dir) == kal_ok, "the test's directory is opened"); + kal_file rec{}; + check(kal_fs_open(dir, "record", 6, (kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate).bits, &rec) == kal_ok, + "a record file is opened"); + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = (kal::fs::dir_recorded | kal::fs::dir_read_only).bits; + rights.record = rec; + kal_dir rd{}; + check(kal_fs_dir_attenuate(dir, &rights, &rd) == kal_ok, "the directory is attenuated, recorded"); + const kal_preopen grant{ rd, "/granted", 8 }; + check(start("--read", &grant, 1, 0) == 0, "a program granted the recorded handle reads through it"); + kal_fs_close_dir(rd); + kal_fs_close_file(rec); + char buf[4096] = {}; + std::snprintf(path, sizeof path, "%s/record", g_dir); + const int fd = ::open(path, O_RDONLY); + const long got = fd >= 0 ? ::read(fd, buf, sizeof buf) : -1; + if (fd >= 0) ::close(fd); + auto holds = [&](const char* entry) { + const std::size_t len = std::strlen(entry) + 1; + for (long i = 0; i + static_cast(len) <= got; ++i) + if (std::memcmp(buf + i, entry, len) == 0 && (i == 0 || buf[i - 1] == 0)) return true; + return false; + }; + check(holds("rpresent.txt"), "the started program's read is in its caller's record"); + check(holds("aabsent.txt"), "and so is the name it found absent"); + check(holds("e"), "and the record of the program that ended is ended"); + + // Confinement, where this kernel has Landlock. + if (kal::process::has(kal::process::confine)) { + kal_dir_rights ro{}; + ro.self_size = sizeof ro; + ro.flags = kal::fs::dir_read_only.bits; + // What a program of this system reads to start --- its loader and + // libraries, wherever the toolchain put them --- and /tmp, read-only; + // /etc is not granted. + static const char* const trees[] = { "tmp", "usr", "lib", "lib64", "home" }; + kal_preopen grants[8]; + static char names[8][16]; + kal_uintptr count = 0; + for (const char* t : trees) { + kal_dir d{}, a{}; + if (kal_fs_open_dir(root(), t, std::strlen(t), &d) != kal_ok) continue; + if (kal_fs_dir_attenuate(d, &ro, &a) != kal_ok) continue; + std::snprintf(names[count], sizeof names[count], "/%s", t); + grants[count] = kal_preopen{ a, names[count], std::strlen(names[count]) }; + ++count; + } + 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); + } else { + std::printf(" this kernel has no Landlock; confinement is refused, which the conformance suite observes\n"); + } + + // Economy. + check(start("--thp", nullptr, 0, kal::process::economy_flag.bits) == 70, + "a program started for economy has no transparent huge pages"); + check(start("--thp", nullptr, 0, kal::process::performance_flag.bits) == 71, + "and one started for performance has them as the system decides"); + + std::snprintf(path, sizeof path, "%s/present.txt", g_dir); ::unlink(path); + std::snprintf(path, sizeof path, "%s/record", g_dir); ::unlink(path); + ::rmdir(g_dir); + if (failures == 0) std::printf("openkal_0_23: ok\n"); + return failures == 0 ? 0 : 1; +}