diff --git a/README.md b/README.md index 6739a8a..fc0c3f2 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.20.0" +openkal = "0.21.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.21.0" +openkal-linux = "0.22.0" ``` ## Why it does not use a C library @@ -102,6 +102,38 @@ 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.22 adds (openkal 0.21) + +**Instructions from a file.** `kal_exec_map_part` maps the file's pages over part +of a region (`mmap` with `MAP_FIXED`, private), read, or read and executed; +`KAL_EXEC_PROP_FILE` is claimed. A volume mounted without execution reports +`kal_err_not_supported`. + +**An ended program's processor time.** `kal_process_info` reports `cpu_ns` of a +started program that has ended from the same record as its peak memory +(`waitid(WNOWAIT)`'s usage), until `kal_process_close` collects it. + +**A program is started without copying this one.** `kal_process_spawn` starts the +program with `clone(CLONE_VM | CLONE_VFORK)` on a stack of its own, as a C +library's `posix_spawn` does, and every request is held until it is replaced; the +started image sets each handled request to its default before it takes the +caller's mask. A copy of the address space (`clone` with `SIGCHLD` alone) copied +the page tables of the whole program and made each page it wrote afterwards fault +again: 1.4 ms in the kernel and about 1 900 faults for each worker a language +server started. + +**An enquiry and an open read the name once.** `kal_fs_info` and `kal_fs_open` +check the name and terminate it for the kernel in one pass, the volume each +enquiry asks about is read without a lock (a sequence read before and after the +slots), and the answer is written straight into the caller's structure: what an +enquiry costs beyond the kernel's own fell from 28 ns to 19 ns. + +**A chunk of `kal_alloc` is carved as it is used.** A free list threaded through +each 1 MiB chunk when it was mapped made the whole chunk resident; six classes in +use made 6 MiB resident in every program. A block is now carved when it is +handed out. An alignment wider than 4096 bytes is mapped, as a block of a class +is aligned only to its size or to the page. + ## What 0.21 adds (openkal 0.20) **Transfers at an offset.** `kal_fs_read_at` and `kal_fs_write_at` are the diff --git a/mcpp.toml b/mcpp.toml index 8bb8165..433efda 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.21.0" +version = "0.22.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" @@ -65,7 +65,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.20.0" +openkal = "0.21.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/exec.cpp b/src/exec.cpp index d04a639..b91f1d8 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1,5 +1,7 @@ #include "sys.h" +#include "handle.h" #include +#include namespace okl { okl_ulong auxval(okl_ulong key); } @@ -114,6 +116,40 @@ int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { 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, +// private, and executable where asked. MAP_FIXED replaces what the part held in +// one call, so there is no moment at which the addresses are free for another +// mapping to take. The arguments are checked first: a refused part leaves the +// region as it was. +int kal_exec_map_part(void* region, kal_uintptr offset, kal_uintptr size, kal_file f, kal_u64 at, + kal_uintptr how) { + const okl_uptr g = page(); + 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) + return kal_err_invalid; + okl::kstat st{}; + const okl_long s = okl::sys(okl::nr_fstat, fd, reinterpret_cast(&st)); + if (okl::failed(s)) return okl::translate(s); + 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); + 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(at)); + if (okl::failed(r)) { + // ENODEV: the file system does not map. EPERM: its files are not + // executed (a volume mounted noexec). The resource refuses, not the + // implementation (exec.h). + if (-r == okl::e_nodev || -r == okl::e_perm) return kal_err_not_supported; + return okl::translate(r); + } + return kal_ok; +} + void kal_exec_free(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return; const okl_uptr bytes = round_up(static_cast(size), page()); @@ -127,7 +163,7 @@ 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; + return KAL_EXEC_PROP_REPUBLISH | KAL_EXEC_PROP_AVAILABLE | KAL_EXEC_PROP_FILE; } } // extern "C" diff --git a/src/fs.cpp b/src/fs.cpp index e0dd925..0cc5ce9 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -228,23 +228,49 @@ kal_uintptr volume_props_of(int fd) { return volume_props(fd); } // A volume's format does not change under a device number while it is mounted, // so the answer is kept by the device the kernel reports for the node; a slot // a later device needs is taken over (eight are kept). +// +// READ WITHOUT A LOCK (0.22): the slots change only when a ninth device is +// asked about, so a reader takes the sequence before and after reading them and +// reads again if a writer was between; writers take the lock. The lock taken on +// every enquiry was a fifth of what an enquiry cost beyond the kernel's own. struct volume_answer { okl_u64 dev; kal_uintptr props; bool known; }; volume_answer g_volumes[8]; unsigned g_volume_next = 0; volatile int g_volume_lock = 0; +unsigned g_volume_sequence = 0; // odd while a writer changes a slot + +bool volume_known(okl_u64 dev, kal_uintptr& props) { + for (;;) { + const unsigned before = __atomic_load_n(&g_volume_sequence, __ATOMIC_ACQUIRE); + if (before & 1u) { okl::sys(okl::nr_sched_yield); continue; } + bool found = false; + kal_uintptr p = 0; + for (auto const& v : g_volumes) + if (__atomic_load_n(&v.known, __ATOMIC_RELAXED) && __atomic_load_n(&v.dev, __ATOMIC_RELAXED) == dev) { + p = __atomic_load_n(&v.props, __ATOMIC_RELAXED); + found = true; + break; + } + __atomic_thread_fence(__ATOMIC_ACQUIRE); + if (__atomic_load_n(&g_volume_sequence, __ATOMIC_RELAXED) != before) continue; + props = p; + return found; + } +} kal_uintptr volume_props_on(okl_u64 dev, int fd) { + kal_uintptr props = 0; + if (volume_known(dev, props)) return props; + props = volume_props(fd); while (__atomic_exchange_n(&g_volume_lock, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); - for (auto const& v : g_volumes) - if (v.known && v.dev == dev) { - const kal_uintptr props = v.props; - __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); - return props; - } - __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); - const kal_uintptr props = volume_props(fd); - while (__atomic_exchange_n(&g_volume_lock, 1, __ATOMIC_ACQUIRE)) okl::sys(okl::nr_sched_yield); - g_volumes[g_volume_next++ % 8] = volume_answer{ dev, props, true }; + __atomic_store_n(&g_volume_sequence, g_volume_sequence + 1, __ATOMIC_RELAXED); + __atomic_thread_fence(__ATOMIC_RELEASE); + volume_answer& slot = g_volumes[g_volume_next++ % 8]; + __atomic_store_n(&slot.known, false, __ATOMIC_RELAXED); + __atomic_store_n(&slot.dev, dev, __ATOMIC_RELAXED); + __atomic_store_n(&slot.props, props, __ATOMIC_RELAXED); + __atomic_store_n(&slot.known, true, __ATOMIC_RELAXED); + __atomic_store_n(&g_volume_sequence, g_volume_sequence + 1, __ATOMIC_RELEASE); __atomic_store_n(&g_volume_lock, 0, __ATOMIC_RELEASE); return props; } @@ -293,8 +319,8 @@ void fill_info(const okl::kstat& st, kal_u32 wanted, kal_node_info* out, v.executable = (st.mode & 0111u) != 0 ? 1 : 0; } - const kal_u32 n = self < sizeof v ? self : (kal_u32)sizeof v; - okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); + if (self >= sizeof v) { *out = v; out->self_size = self; return; } + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), self); } // A node that refers to nothing, in the same shape. @@ -453,8 +479,8 @@ int kal_fs_open_dir(kal_dir base, const char* name, kal_uintptr len, kal_dir* ou int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_file* out) { 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; + if (b < 0 || out == nullptr) return kal_err_invalid; + okl::accepted_name t(name, len); if (!t.ok) return kal_err_invalid; const bool r = (flags & KAL_OPEN_READ) != 0; const bool w = (flags & KAL_OPEN_WRITE) != 0; @@ -556,8 +582,8 @@ kal_intptr kal_fs_write_at(kal_file f, const void* buf, kal_uintptr len, kal_u64 int kal_fs_info(kal_dir base, const char* name, kal_uintptr len, kal_uintptr flags, kal_u32 wanted, kal_node_info* out) { const int b = okl::unpack(base.h); - if (b < 0 || !info_ok(out) || !okl::acceptable(name, len)) return kal_err_invalid; - okl::terminated t(name, len); if (!t.ok) return kal_err_invalid; + if (b < 0 || !info_ok(out)) return kal_err_invalid; + okl::accepted_name t(name, len); if (!t.ok) return kal_err_invalid; okl::kstat st{}; // RESOLVES BY DEFAULT, SO THAT ASKING AND OPENING ANSWER THE SAME QUESTION. // diff --git a/src/memory.cpp b/src/memory.cpp index 5449d45..4173f3b 100644 --- a/src/memory.cpp +++ b/src/memory.cpp @@ -43,6 +43,9 @@ struct spin { spin g_lock; void* g_free[kClasses]; +// The part of each class's newest chunk no block has been carved from yet. +unsigned char* g_next[kClasses]; +unsigned char* g_end[kClasses]; int class_of(okl_uptr n) { okl_uptr s = kMinBlock; int c = 0; @@ -66,19 +69,29 @@ void unmap(void* p, okl_uptr bytes) { okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); } // A chunk is page-aligned and a block size is a power of two no larger than a -// chunk, so a block at a multiple of its size is aligned to its size. That is -// why an alignment request is satisfied by choosing a larger class rather than -// by padding: no metadata is needed, and none is kept. -bool refill(int c) { +// chunk, so a block at a multiple of its size is aligned to its size or to the +// page, whichever is smaller. That is why an alignment request up to 4096 bytes +// (the smallest page among the environments) is satisfied by choosing a larger +// class rather than by padding: no metadata is needed, and none is kept. A +// wider alignment is mapped (below), as a block of a class would not have it. +// +// A CHUNK IS CARVED AS ITS BLOCKS ARE HANDED OUT, NOT WHEN IT IS MAPPED. A free +// list threaded through the whole chunk at once wrote a pointer into each of +// its blocks, so every page of every chunk was made resident before a block of +// it was used: measured, six classes and six mebibytes resident, 1 500 page +// faults, in each program an ok image's loader started --- three quarters of +// what such a program paid before its first instruction. +void* carve(int c) { const okl_uptr bs = size_of(c); - auto* base = static_cast(map(kChunk)); - if (!base) return false; - for (okl_uptr off = kChunk; off >= bs; off -= bs) { - auto* b = base + off - bs; - *reinterpret_cast(b) = g_free[c]; - g_free[c] = b; + if (g_next[c] == g_end[c]) { + auto* base = static_cast(map(kChunk)); + if (!base) return nullptr; + g_next[c] = base; + g_end[c] = base + kChunk; } - return true; + void* b = g_next[c]; + g_next[c] += bs; + return b; } } // namespace @@ -90,12 +103,12 @@ void* kal_alloc(kal_uintptr size, kal_uintptr align) { if (align < kMinBlock) align = kMinBlock; okl_uptr want = size < align ? align : size; - if (want <= kMaxSmall) { + if (want <= kMaxSmall && align <= 4096) { const int c = class_of(want); g_lock.lock(); - if (!g_free[c] && !refill(c)) { g_lock.unlock(); return nullptr; } void* b = g_free[c]; - g_free[c] = *reinterpret_cast(b); + if (b) g_free[c] = *reinterpret_cast(b); + else b = carve(c); g_lock.unlock(); return b; } @@ -128,7 +141,7 @@ void kal_free(void* p, kal_uintptr size, kal_uintptr align) { if (align < kMinBlock) align = kMinBlock; const okl_uptr want = size < align ? align : size; - if (want <= kMaxSmall) { + if (want <= kMaxSmall && align <= 4096) { const int c = class_of(want); g_lock.lock(); *reinterpret_cast(p) = g_free[c]; diff --git a/src/process.cpp b/src/process.cpp index 50e3483..632a223 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -5,11 +5,14 @@ #include namespace okl { extern char** g_envp; extern bool g_pipe_started_ignored; okl_ulong auxval(okl_ulong key); } +// The transfer to a function on a stack of its own (task.cpp). +namespace okl { okl_long clone_on_stack(int (*fn)(void*), void* stack, int flags, void* arg); } // Defined with what a unit uses, below (0.21). namespace okl { int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& ended); 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); } namespace { @@ -353,9 +356,9 @@ inline okl_long handle_pid(kal_process h) { return static_cast(h.h & 0 // where `execveat' is unusable: from the first start under a translator this // implementation recognises, and, elsewhere, by each start that meets ENOSYS, // which falls back in the started image itself. That fallback is learned by -// no other start: the image is a copy made by `clone' without shared memory, -// so nothing it learns reaches this program, and the price is one refused -// system call per start. +// no other start: the image writes nothing of this program's (start_plan), so +// nothing it learns reaches this program, and the price is one refused system +// call per start. // // Ported from the copy mcppls carried (vendor/openkal-linux 0.15.1), which set // the flag itself under PRoot; recognising the translator here removes that. @@ -451,6 +454,268 @@ static bool entries_well_formed(const char** envp, const kal_uintptr* lens, kal_ return true; } +namespace { + +// WHAT THE STARTED IMAGE IS GIVEN, prepared before it exists. It runs in this +// program's memory until it is replaced, so it reads this and writes nothing of +// 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. +struct start_plan { + const kal_spawn* how; + const okl::terminated* name; + const okl_long* granted; + int b, w; + okl_long in, ou, er; + const vector* args; + const vector* envs; + const exec_report* report; + const char* resolved; + bool named, use_name, unit, bind; + okl_long join, mine; + const unsigned long* mask; +}; + +constexpr okl_uptr kStartStack = 64 * 1024; + +// THE STARTED IMAGE, until it is replaced: on its own stack, in this program's +// memory, with every request held (kal_process_spawn). +int start_image(void* arg) { + const start_plan& plan = *static_cast(arg); + const kal_spawn* const how = plan.how; + const okl::terminated& p = *plan.name; + const exec_report& report = *plan.report; + const vector& args = *plan.args; + const vector& envs = *plan.envs; + const int b = plan.b, w = plan.w; + const okl_long in = plan.in, ou = plan.ou, er = plan.er, join = plan.join, mine = plan.mine; + const bool named = plan.named, use_name = plan.use_name, unit = plan.unit, bind = plan.bind; + const char* const resolved = plan.resolved; + okl_long granted[16]; + for (kal_uintptr i = 0; i < how->grant_count; ++i) granted[i] = plan.granted[i]; + + // A HANDLER OF THE CALLER'S IS NOT ENTERED HERE: each request it handles + // is given its default until the replacement, which gives every handled + // request its default in any case. One it ignores stays ignored, as the + // replacement keeps it. + for (int signo = 1; signo <= 64; ++signo) { + if (signo == 9 || signo == 19) continue; // SIGKILL, SIGSTOP + struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } act{}; + if (okl::failed(okl::sys(okl::nr_rt_sigaction, signo, 0, reinterpret_cast(&act), + sizeof act.mask))) + continue; + if (act.handler == nullptr || act.handler == reinterpret_cast(1)) continue; + decltype(act) standard{}; + okl::sys(okl::nr_rt_sigaction, signo, reinterpret_cast(&standard), 0, sizeof standard.mask); + } + + // WHAT THE STARTED PROGRAM RECEIVES IS THE THREE STREAMS AND THE GRANTED + // DIRECTORIES, AND NOTHING ELSE (SPEC.md clause 7.13). + // + // Every source is first moved above the positions being filled, because + // placing one source on its position must not overwrite another that + // still has to be read: a granted directory, a stream, the base or the + // working directory may each occupy a number between 0 and 3+n. Placing + // them one at a time where they stood is how a grant named `work' + // arrived as a second copy of `/', and how a placement overwrote the + // base the program's name is resolved under. A source that is moved has + // its own descriptor flag; `dup3' then clears it on the copy it places, + // 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); + 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)) { + report.say(r); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + return r; + }; + const okl_long sin = in != 0 ? lift(in) : 0; + const okl_long sou = ou != 0 ? lift(ou) : 0; + const okl_long ser = er != 0 ? lift(er) : 0; + for (kal_uintptr i = 0; i < how->grant_count; ++i) granted[i] = lift(granted[i]); + const okl_long base = lift(b); + const okl_long work = lift(w); + + if (sin != 0) okl::sys(okl::nr_dup3, sin, 0, 0); + if (sou != 0) okl::sys(okl::nr_dup3, sou, 1, 0); + 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); + + // What the CALLER itself inherited without the flag is not a handle it + // granted. A program that starts others inside a sandbox passes every + // descriptor it holds to them, and one of those reaching beyond the + // sandbox is exactly what the sandbox was for. + close_on_exec_from(top); + + envs.stamp(okl::sys(okl::nr_getpid)); + + // THE DIRECTORY THE PROGRAM RUNS IN, AND THIS LINE IS THE WHOLE OF IT. + // + // `openat' below takes the base as a dirfd, but that only RESOLVES the + // name --- resolving a name is not entering a directory, which is what + // the comment here used to get wrong. Until 0.11 there was no second + // directory to enter, and a started program ran wherever this + // implementation happened to be. + // + // A FAILURE HERE MUST NOT REACH `execveat'. Running the right program + // in the wrong directory is precisely the silent wrongness this exists to + // remove, so it is reported through the same pipe an exec failure uses. + if (const okl_long e = okl::sys(okl::nr_fchdir, work); okl::failed(e)) { + report.say(e); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + + // THE UNIT, ENTERED HERE AND NOT FROM THE PARENT: the parent's own + // `setpgid' on this child races the replacement below and loses once the + // program has been replaced. Zero means "your own", which is how a group + // comes into existence at all --- there is nothing to create beforehand, + // which is why the interface reports the identity rather than taking it. + if (unit) okl::sys(okl::nr_setpgid, 0, join); + + // SIGPIPE AS THIS PROGRAM WAS STARTED WITH, not as this implementation + // set it for its own writes (env.cpp): an ignore would outlive `execve'. + if (!okl::g_pipe_started_ignored) { + struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } + standard{ nullptr /* SIG_DFL */, 0, nullptr, 0 }; + okl::sys(okl::nr_rt_sigaction, 13 /* SIGPIPE */, + reinterpret_cast(&standard), 0, sizeof standard.mask); + } + + if (bind) { + // 9 is SIGKILL: the binding must not be something the started program + // can decline, because the caller asked for a program that does not + // outlive it and not for one that is invited not to. + okl::sys(okl::nr_prctl, okl::pr_set_pdeathsig, 9, 0, 0, 0); + // The window: if the caller ended between the clone and the line + // above, the signal is already spent and this image would survive it. + if (okl::sys(okl::nr_getppid) != mine) + okl::sys(okl::nr_exit_group, 127); + } + + // 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, + sizeof *plan.mask); + + // WHAT THE STARTED PROGRAM KEEPS IS A DESCRIPTOR FOR THE PROGRAM, NOT + // FOR THE DIRECTORY IT WAS FOUND IN, AND WITHOUT A DESCRIPTOR AT ALL A + // WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED. + // + // `execveat' with a dirfd and a relative name gives the program's name to + // the kernel as `/dev/fd//'. That spelling is invisible to a + // caller and harmless for an ordinary executable --- the kernel already + // holds the file open. It stops being harmless the moment the program + // needs an INTERPRETER: a `#!' script, or a binary of another + // architecture registered through `binfmt_misc'. The kernel then starts + // the interpreter and hands it that name to open --- AFTER the + // replacement, by which time a close-on-exec descriptor is gone. The + // interpreter is told the script does not exist. + // + // Measured in twenty lines of plain C, with everything else identical: + // + // descriptor WITH O_CLOEXEC execveat -> ENOENT + // descriptor WITHOUT O_CLOEXEC STARTED ok + // + // It is not a property of one architecture. It was FOUND on aarch64, + // where every foreign binary needs the binfmt interpreter and so every + // start failed --- and it was mistaken there for a limit of the emulator. + // It reproduces natively on x86_64 with a `#!' script, which is what a + // consumer meets on any machine. + // + // The descriptor that survives must not be the base. A copy of the base + // is a handle on a whole directory, often `/', and a program that runs + // other programs in a sandbox hands it on to them, where + // `/proc/self/fd//' reaches whatever the sandbox hid. So the program + // itself is opened here, with `O_PATH' because it is only to be named + // and not read, and `execveat' is given that descriptor and an empty + // name. The interpreter's `/dev/fd/' then names the file, and nothing + // can be walked out of a descriptor for a file. + // + // AND ONLY A PROGRAM THAT NEEDS AN INTERPRETER KEEPS EVEN THAT. The + // descriptor is opened with `O_CLOEXEC', and an ordinary executable, + // which the kernel holds open itself, starts with nothing left behind. + // A program that needs an interpreter is refused with ENOENT BEFORE the + // point of no return --- `binfmt_script' and `binfmt_misc' test whether + // the name will still be reachable and answer that it will not --- so + // the image is still here to clear the flag and ask again. The other + // sources of ENOENT on an open descriptor, an interpreter that does not + // exist, answer the same the second time. + // + // The descriptor is moved above the placed positions as well: where + // the caller had no standard input and supplied none, the lowest free + // number is 0, and a script would read its own descriptor as input. + // + // A SCRIPT IS GIVEN ITS NAME WHERE THE NAME REACHES THE SAME FILE + // (0.18.1). Through the descriptor a script sees `$0' as `/dev/fd/', + // and one that locates its neighbours with `dirname "$0"' --- a wrapper + // installed beside the files it runs, a test's stub provider --- finds + // none of them: measured in mcpp, whose provider stub wrote beside + // itself into `/dev/fd/ticks'. So a program that needs an interpreter is + // started by its absolute name, as every shell and `posix_spawn' start + // it, when that name is known and names the file the descriptor holds + // (the same device and inode, so the name grants nothing the descriptor + // did not). Only where it does not --- a base the kernel cannot name, a + // name rebound since it was opened --- is the descriptor kept, with the + // price stated above: the descriptor still names the program and can be + // reopened through `/proc/self/fd/', as `/proc/self/exe' can. + // + // Opened HERE, in the started image, and not where the preopens are made: + // the caller's own descriptors stay close-on-exec, which is what every + // other operation of this implementation relies upon. + // + // A name that cannot be opened is reported exactly as one that cannot be + // started is, through the same pipe. + if (use_name) { + const okl_long why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots)); + report.say(why); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + okl_long exe = okl::sys(okl::nr_openat, base, reinterpret_cast(p.buf), + okl::o_path | okl::o_cloexec, 0); + if (okl::failed(exe)) { + report.say(exe); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + if (exe < top) exe = lift(exe); + + auto start = [&] { + return okl::sys(okl::nr_execveat, exe, reinterpret_cast(""), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots), okl::at_empty_path); + }; + okl_long why = start(); + if (why == -okl::e_noent && named && same_file(exe, resolved)) + why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots)); + if (why == -okl::e_noent) { + okl::sys(okl::nr_fcntl, exe, okl::f_setfd, 0); + why = start(); + } + if (why == -okl::e_nosys && named) + why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots)); + // Reached only when the replacement did not happen, because when it does + // there is nothing here to reach. + report.say(why); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + +} + +} // namespace + int kal_process_spawn(const kal_spawn* how, const char* path, kal_uintptr path_len, const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, @@ -523,209 +788,38 @@ int kal_process_spawn(const kal_spawn* how, exec_report report; report.open(how->grant_count); - const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); + // 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 + // for the replacement, as a C library's `posix_spawn' starts a program. A + // copy of the address space (clone with SIGCHLD alone, until 0.22) copied the + // page tables of the whole program and marked every page it had written to + // be copied, so that each page it wrote afterwards faulted again: measured in + // a language server that starts a worker for each module it builds, 1.4 ms in + // the kernel for each start and a third of a million faults over 176 starts, + // a fifth of its processor time. + // + // Every request is held until the image is replaced. A handler of this + // program run in the image would run in this program's memory; so the image + // sets every handler to the default first, and only then takes the mask the + // caller had (start_image). + unsigned long every = ~0ul, caller_mask = 0; + 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 }; + 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); + const okl_long child = okl::failed(stack) + ? stack + : okl::clone_on_stack(&start_image, reinterpret_cast(stack) + kStartStack, + 0x100 /* CLONE_VM */ | 0x4000 /* CLONE_VFORK */ | 17 /* SIGCHLD */, + const_cast(&plan)); + okl::sys(okl::nr_rt_sigprocmask, 2 /* SIG_SETMASK */, reinterpret_cast(&caller_mask), 0, + sizeof caller_mask); + if (!okl::failed(stack)) okl::sys(okl::nr_munmap, stack, static_cast(kStartStack)); if (okl::failed(child)) { report.close_both(); return okl::translate(child); } - if (child == 0) { - // WHAT THE STARTED PROGRAM RECEIVES IS THE THREE STREAMS AND THE GRANTED - // DIRECTORIES, AND NOTHING ELSE (SPEC.md clause 7.13). - // - // Every source is first moved above the positions being filled, because - // placing one source on its position must not overwrite another that - // still has to be read: a granted directory, a stream, the base or the - // working directory may each occupy a number between 0 and 3+n. Placing - // them one at a time where they stood is how a grant named `work' - // arrived as a second copy of `/', and how a placement overwrote the - // base the program's name is resolved under. A source that is moved has - // its own descriptor flag; `dup3' then clears it on the copy it places, - // 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); - 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)) { - report.say(r); - okl::sys(okl::nr_exit_group, 127); - for (;;) { } - } - return r; - }; - const okl_long sin = in != 0 ? lift(in) : 0; - const okl_long sou = ou != 0 ? lift(ou) : 0; - const okl_long ser = er != 0 ? lift(er) : 0; - for (kal_uintptr i = 0; i < how->grant_count; ++i) granted[i] = lift(granted[i]); - const okl_long base = lift(b); - const okl_long work = lift(w); - - if (sin != 0) okl::sys(okl::nr_dup3, sin, 0, 0); - if (sou != 0) okl::sys(okl::nr_dup3, sou, 1, 0); - 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); - - // What the CALLER itself inherited without the flag is not a handle it - // granted. A program that starts others inside a sandbox passes every - // descriptor it holds to them, and one of those reaching beyond the - // sandbox is exactly what the sandbox was for. - close_on_exec_from(top); - - envs.stamp(okl::sys(okl::nr_getpid)); - - // THE DIRECTORY THE PROGRAM RUNS IN, AND THIS LINE IS THE WHOLE OF IT. - // - // `openat' below takes the base as a dirfd, but that only RESOLVES the - // name --- resolving a name is not entering a directory, which is what - // the comment here used to get wrong. Until 0.11 there was no second - // directory to enter, and a started program ran wherever this - // implementation happened to be. - // - // A FAILURE HERE MUST NOT REACH `execveat'. Running the right program - // in the wrong directory is precisely the silent wrongness this exists to - // remove, so it is reported through the same pipe an exec failure uses. - if (const okl_long e = okl::sys(okl::nr_fchdir, work); okl::failed(e)) { - report.say(e); - okl::sys(okl::nr_exit_group, 127); - for (;;) { } - } - - // THE UNIT, ENTERED HERE AND NOT FROM THE PARENT: the parent's own - // `setpgid' on this child races the replacement below and loses once the - // program has been replaced. Zero means "your own", which is how a group - // comes into existence at all --- there is nothing to create beforehand, - // which is why the interface reports the identity rather than taking it. - if (unit) okl::sys(okl::nr_setpgid, 0, join); - - // SIGPIPE AS THIS PROGRAM WAS STARTED WITH, not as this implementation - // set it for its own writes (env.cpp): an ignore would outlive `execve'. - if (!okl::g_pipe_started_ignored) { - struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } - standard{ nullptr /* SIG_DFL */, 0, nullptr, 0 }; - okl::sys(okl::nr_rt_sigaction, 13 /* SIGPIPE */, - reinterpret_cast(&standard), 0, sizeof standard.mask); - } - - if (bind) { - // 9 is SIGKILL: the binding must not be something the started program - // can decline, because the caller asked for a program that does not - // outlive it and not for one that is invited not to. - okl::sys(okl::nr_prctl, okl::pr_set_pdeathsig, 9, 0, 0, 0); - // The window: if the caller ended between the clone and the line - // above, the signal is already spent and this image would survive it. - if (okl::sys(okl::nr_getppid) != mine) - okl::sys(okl::nr_exit_group, 127); - } - - // WHAT THE STARTED PROGRAM KEEPS IS A DESCRIPTOR FOR THE PROGRAM, NOT - // FOR THE DIRECTORY IT WAS FOUND IN, AND WITHOUT A DESCRIPTOR AT ALL A - // WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED. - // - // `execveat' with a dirfd and a relative name gives the program's name to - // the kernel as `/dev/fd//'. That spelling is invisible to a - // caller and harmless for an ordinary executable --- the kernel already - // holds the file open. It stops being harmless the moment the program - // needs an INTERPRETER: a `#!' script, or a binary of another - // architecture registered through `binfmt_misc'. The kernel then starts - // the interpreter and hands it that name to open --- AFTER the - // replacement, by which time a close-on-exec descriptor is gone. The - // interpreter is told the script does not exist. - // - // Measured in twenty lines of plain C, with everything else identical: - // - // descriptor WITH O_CLOEXEC execveat -> ENOENT - // descriptor WITHOUT O_CLOEXEC STARTED ok - // - // It is not a property of one architecture. It was FOUND on aarch64, - // where every foreign binary needs the binfmt interpreter and so every - // start failed --- and it was mistaken there for a limit of the emulator. - // It reproduces natively on x86_64 with a `#!' script, which is what a - // consumer meets on any machine. - // - // The descriptor that survives must not be the base. A copy of the base - // is a handle on a whole directory, often `/', and a program that runs - // other programs in a sandbox hands it on to them, where - // `/proc/self/fd//' reaches whatever the sandbox hid. So the program - // itself is opened here, with `O_PATH' because it is only to be named - // and not read, and `execveat' is given that descriptor and an empty - // name. The interpreter's `/dev/fd/' then names the file, and nothing - // can be walked out of a descriptor for a file. - // - // AND ONLY A PROGRAM THAT NEEDS AN INTERPRETER KEEPS EVEN THAT. The - // descriptor is opened with `O_CLOEXEC', and an ordinary executable, - // which the kernel holds open itself, starts with nothing left behind. - // A program that needs an interpreter is refused with ENOENT BEFORE the - // point of no return --- `binfmt_script' and `binfmt_misc' test whether - // the name will still be reachable and answer that it will not --- so - // the image is still here to clear the flag and ask again. The other - // sources of ENOENT on an open descriptor, an interpreter that does not - // exist, answer the same the second time. - // - // The descriptor is moved above the placed positions as well: where - // the caller had no standard input and supplied none, the lowest free - // number is 0, and a script would read its own descriptor as input. - // - // A SCRIPT IS GIVEN ITS NAME WHERE THE NAME REACHES THE SAME FILE - // (0.18.1). Through the descriptor a script sees `$0' as `/dev/fd/', - // and one that locates its neighbours with `dirname "$0"' --- a wrapper - // installed beside the files it runs, a test's stub provider --- finds - // none of them: measured in mcpp, whose provider stub wrote beside - // itself into `/dev/fd/ticks'. So a program that needs an interpreter is - // started by its absolute name, as every shell and `posix_spawn' start - // it, when that name is known and names the file the descriptor holds - // (the same device and inode, so the name grants nothing the descriptor - // did not). Only where it does not --- a base the kernel cannot name, a - // name rebound since it was opened --- is the descriptor kept, with the - // price stated above: the descriptor still names the program and can be - // reopened through `/proc/self/fd/', as `/proc/self/exe' can. - // - // Opened HERE, in the started image, and not where the preopens are made: - // the caller's own descriptors stay close-on-exec, which is what every - // other operation of this implementation relies upon. - // - // A name that cannot be opened is reported exactly as one that cannot be - // started is, through the same pipe. - if (use_name) { - const okl_long why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots)); - report.say(why); - okl::sys(okl::nr_exit_group, 127); - for (;;) { } - } - okl_long exe = okl::sys(okl::nr_openat, base, reinterpret_cast(p.buf), - okl::o_path | okl::o_cloexec, 0); - if (okl::failed(exe)) { - report.say(exe); - okl::sys(okl::nr_exit_group, 127); - for (;;) { } - } - if (exe < top) exe = lift(exe); - - auto start = [&] { - return okl::sys(okl::nr_execveat, exe, reinterpret_cast(""), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots), okl::at_empty_path); - }; - okl_long why = start(); - if (why == -okl::e_noent && named && same_file(exe, resolved)) - why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots)); - if (why == -okl::e_noent) { - okl::sys(okl::nr_fcntl, exe, okl::f_setfd, 0); - why = start(); - } - if (why == -okl::e_nosys && named) - why = okl::sys(okl::nr_execve, reinterpret_cast(resolved), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots)); - // Reached only when the replacement did not happen, because when it does - // there is nothing here to reach. - report.say(why); - okl::sys(okl::nr_exit_group, 127); - for (;;) { } - } - if (const okl_long why = report.heard()) { reap(child); return okl::translate(why); @@ -1208,9 +1302,13 @@ int kal_process_info(kal_process p, kal_u32 wanted, kal_program_info* out) { v.present |= KAL_PINFO_STARTED; } } - if (!ended && (wanted & KAL_PINFO_CPU)) { + // CPU (0.21): of a started program that has ended, from the record the + // kernel keeps until it is collected, as its peak memory below. + if (wanted & KAL_PINFO_CPU) { okl_u64 c = 0; - if (cpu_ns(pid, me, c)) { v.cpu_ns = c; v.present |= KAL_PINFO_CPU; } + const bool ok = !ended ? cpu_ns(pid, me, c) + : handle_kind(p) == kind_child && okl::child_cpu(pid, c); + if (ok) { v.cpu_ns = c; v.present |= KAL_PINFO_CPU; } } // 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 @@ -1509,6 +1607,14 @@ bool child_memory(okl_long pid, bool ended, okl_u64& now, okl_u64& peak) { return true; } +bool child_cpu(okl_long pid, okl_u64& ns) { + k_rusage ru{}; + if (!ended_usage(pid, ru)) return false; + ns = static_cast(ru.utime_s + ru.stime_s) * 1000000000ull + + static_cast(ru.utime_us + ru.stime_us) * 1000ull; + return true; +} + // How a started program ended, read without collecting it; `block' waits for // it. Shared with openkal.timeout. int child_outcome(okl_long pid, bool block, int* status, int* terminated, bool& ended) { diff --git a/src/sys.h b/src/sys.h index 571b6ec..4135dd6 100644 --- a/src/sys.h +++ b/src/sys.h @@ -680,4 +680,28 @@ struct terminated { } }; +// A name `acceptable' accepts, terminated for the kernel, in one pass over it: +// the operations a program makes most often (an enquiry, an open) read each +// name once rather than twice (0.22). +struct accepted_name { + char buf[max_name + 1]; + bool ok = false; + accepted_name(const char* name, okl_uptr len) { + if (name == nullptr || len == 0 || len >= sizeof buf || name[0] == '/') return; + okl_uptr start = 0; + for (okl_uptr i = 0; i < len; ++i) { + const char c = name[i]; + buf[i] = c; + if (c != '/') continue; + const okl_uptr n = i - start; + if (n == 0 || (n == 2 && name[start] == '.' && name[start + 1] == '.')) return; + start = i + 1; + } + const okl_uptr n = len - start; + if (n == 0 || (n == 2 && name[start] == '.' && name[start + 1] == '.')) return; + buf[len] = '\0'; + ok = true; + } +}; + } // namespace okl diff --git a/src/task.cpp b/src/task.cpp index a9e1db2..6de61f8 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -22,8 +22,6 @@ namespace okl { okl_ulong auxval(okl_ulong key); } // is not an optimisation: in that arrangement the choice is between creating // contexts here and not providing openkal.task at all. -#ifdef OKL_STANDALONE - // The child of a clone begins on a stack of its own with no return address, so // the transfer cannot be written in C. The sequence is the one every C library // uses, and it is short enough to read: the arguments are moved into the @@ -84,10 +82,18 @@ __asm__( ); #endif +// In both arrangements: a program is started by it as well (process.cpp, +// through okl::clone_on_stack below). extern "C" okl_long __okl_clone(int (*fn)(void*), void* stack, int flags, void* arg, int* ptid, void* tls, int* ctid); -#else +namespace okl { +okl_long clone_on_stack(int (*fn)(void*), void* stack, int flags, void* arg) { + return __okl_clone(fn, stack, flags, arg, nullptr, nullptr, nullptr); +} +} // namespace okl + +#ifndef OKL_STANDALONE // THE HOST'S OWN ENQUIRY, WHICH IS A NAME THIS IMPLEMENTATION MAY TAKE IN THIS // ARRANGEMENT AND ONLY IN THIS ONE. A program that carries a runtime is a // program whose runtime owns the stacks, and `pthread_getattr_np' is how that diff --git a/tests/openkal_0_21.cpp b/tests/openkal_0_21.cpp new file mode 100644 index 0000000..e6828ea --- /dev/null +++ b/tests/openkal_0_21.cpp @@ -0,0 +1,62 @@ +// What openkal-linux 0.22 changes beside openkal 0.21, as this kernel does it. +// +// A CHUNK IS RESIDENT ONLY WHERE ITS BLOCKS WERE HANDED OUT: one block of each +// of the twelve classes makes far less than a mebibyte resident, where a free +// list threaded through each chunk at once made twelve. +// +// AN ALIGNMENT WIDER THAN A PAGE IS HONOURED for a size a class would hold: a +// block of a class is aligned to its size or to the page, whichever is less. +import openkal.memory; +import openkal.process; +import openkal.stream; +import openkal.types; + +namespace { +int failures = 0; +void check(bool ok, const char* what) { + if (ok) return; + ++failures; + const char pre[] = "FAIL: "; + kal::write(kal::err(), pre, sizeof(pre) - 1); + kal_uintptr n = 0; while (what[n]) ++n; + kal::write(kal::err(), what, n); + kal::write(kal::err(), "\n", 1); +} + +kal_u64 resident() { + kal_process self{}; + if (kal_process_self(&self) != kal_ok) return 0; + kal_program_info info{}; + info.self_size = sizeof info; + const int e = kal_process_info(self, kal::process::info::memory, &info); + kal_process_close(self); + return e == kal_ok && (info.present & kal::process::info::memory) ? info.memory_bytes : 0; +} +} // namespace + +int main() { + { + void* blocks[12] = {}; + const kal_u64 before = resident(); + kal_uintptr size = 16; + for (auto& b : blocks) { b = kal_alloc(size, 16); size <<= 1; } + const kal_u64 after = resident(); + bool all = true; + for (auto* b : blocks) all = all && b != nullptr; + check(all, "a block of each class is allocated"); + check(before != 0 && after < before + (kal_u64{1} << 20), + "one block of each class makes less than a mebibyte resident"); + size = 16; + for (auto* b : blocks) { kal_free(b, size, 16); size <<= 1; } + } + { + const kal_uintptr aligns[] = { 8192, 16384, 32768 }; + for (const kal_uintptr a : aligns) { + void* p = kal_alloc(100, a); + check(p != nullptr && reinterpret_cast(p) % a == 0, + "a small allocation is aligned to an alignment wider than the page"); + kal_free(p, 100, a); + } + } + return failures == 0 ? 0 : 1; +}