diff --git a/README.md b/README.md index 11817e3..db1f227 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ for Linux, written on the kernel's own system-call interface. openkal = "0.9.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.8.0" +openkal-linux = "0.9.0" ``` ## Why it does not use a C library diff --git a/mcpp.toml b/mcpp.toml index acbcfe7..d7e690a 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.8.0" +version = "0.9.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" @@ -18,7 +18,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-linux" [dependencies] -openkal = "0.9.0" +openkal = "0.10.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/fs.cpp b/src/fs.cpp index e252bc7..940c38e 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -275,6 +275,33 @@ int kal_fs_set_modified(kal_file f, kal_u64 modified_ns) { return okl::failed(r) ? okl::translate(r) : kal_ok; } +// The same, upon a NAME. Version 0.10. +// +// ⚠️⚠️ ADDED BECAUSE THE FORM ABOVE CANNOT REACH A DIRECTORY, AND A CONSUMER +// PAID FOR THAT. `kal_fs_set_modified' takes a `kal_file'; a directory is opened +// as a `kal_dir'; there was no third thing. openkal-musl reached a lock +// directory's timestamp by opening the directory for READING and setting the +// time on that, which worked here and was outside anything the interface said. +// This is the stated route. +int kal_fs_set_modified_at(kal_dir base, const char* name, kal_uintptr len, + kal_u64 modified_ns) { + 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; + + constexpr okl_i64 utime_omit = 0x3ffffffe; + okl::ktimespec times[2]; + times[0].sec = 0; times[0].nsec = utime_omit; + times[1].sec = static_cast(modified_ns / 1000000000u); + times[1].nsec = static_cast(modified_ns % 1000000000u); + + // Resolves, because opening resolves and this is stated to agree with it. + 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; +} + 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; @@ -379,8 +406,15 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, // them. kal_uintptr kal_fs_props(kal_dir d) { const int fd = okl::unpack(d.h); + // ⭐ LOCKS AND CAPACITY ARE IN THE CONSERVATIVE SET, and that is a claim + // about this kernel rather than about the volume: an open-file lock and + // `fstatfs' are answered by the VFS for every format beneath it, including + // the read-only ones --- a lock excludes writers a read-only volume does not + // have, which is a true answer and not a useful one. A format that could not + // would have to be excluded by name here, and this kernel has none. const kal_uintptr conservative = - KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME; + KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME + | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; if (fd < 0) return 0; okl::kstatfs sf{}; @@ -399,9 +433,10 @@ kal_uintptr kal_fs_props(kal_dir d) { // rename cannot be atomic because there is no rename. case okl::fs_squashfs: case okl::fs_erofs: return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE - | KAL_FS_PROP_LINKS; + | KAL_FS_PROP_LINKS | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; case okl::fs_iso9660: - return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE; + return KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_CASE_SENSITIVE + | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; // The FAT family stores neither a case distinction nor a node that // names another. `symlink' on such a volume reports EPERM, and this is @@ -418,6 +453,97 @@ kal_uintptr kal_fs_props(kal_dir d) { } } +// --- exclusion upon a range of a file -------------------------------------- +// +// ⭐⭐ THE OPEN-FILE FORM, AND THE DIFFERENCE IS THE WHOLE REASON THIS IS +// WORTH SPECIFYING. +// +// This kernel's oldest record lock is held by the PROCESS and is released as +// soon as that process closes ANY descriptor for the node --- so a library that +// opened one file twice destroyed its own lock, and two parts of one program +// could not exclude each other at all. openkal states that the holder is the +// `kal_file', which is exactly what `F_OFD_*' describes: the lock belongs to the +// open file description and ends when the last descriptor for it closes, and +// when the program ends however it ends. +// +// ⚠️ Releasing on death is the half a caller cannot build for itself. Exclusion +// it can: `KAL_OPEN_EXCLUSIVE' and a name beside the file. What nothing above +// this line can do is release that name when its holder dies, so a program that +// ended abnormally while holding one would be locked out of its own file for +// ever. +static int lock_range(kal_file f, kal_u64 start, kal_u64 len, + short type, bool wait) { + const int fd = okl::unpack(f.h); + if (fd < 0) return kal_err_invalid; + + okl::kflock fl{}; + fl.l_type = type; + fl.l_whence = okl::seek_set; + fl.l_start = static_cast(start); + // openkal spells "to the end, however far that comes to be" as zero, and so + // does this kernel. The two agree, so nothing is translated. + fl.l_len = static_cast(len); + + const okl_long cmd = wait ? okl::f_ofd_setlkw : okl::f_ofd_setlk; + okl_long r; + do { + r = okl::sys(okl::nr_fcntl, fd, cmd, reinterpret_cast(&fl)); + } while (okl::interrupted(r)); + if (!okl::failed(r)) return kal_ok; + + // ⚠️⚠️ TWO VALUES MEAN ONE THING HERE, AND openkal NAMES ONE OF THEM. + // + // The standard this call comes from says a range another holder has is + // reported as EITHER of two values, and leaves the choice to the system --- + // so a caller must accept both and an implementation of openkal must not + // pass that choice on. openkal says `kal_err_again', which is the answer a + // caller polls upon; the other value translates to `permission', which a + // caller reads as "asking again will not help" and acts upon by stopping. + // + // ⭐ Narrowed to the attempt that does NOT wait, because that is the only + // one for which the two values carry this meaning. A permission failure on + // any other path keeps its own answer. + if (!wait && (-r == okl::e_acces || -r == okl::e_again)) return kal_err_again; + return okl::translate(r); +} + +int kal_fs_lock(kal_file f, kal_u64 start, kal_u64 len, kal_uintptr mode) { + const bool shared = (mode & KAL_LOCK_SHARED) != 0; + const bool exclusive = (mode & KAL_LOCK_EXCLUSIVE) != 0; + // One of the two, and not both and not neither: a caller that asked for + // both asked for something no environment has, and one that asked for + // neither did not say what it wanted. + if (shared == exclusive) return kal_err_invalid; + return lock_range(f, start, len, + shared ? okl::lock_read : okl::lock_write, + (mode & KAL_LOCK_WAIT) != 0); +} + +int kal_fs_unlock(kal_file f, kal_u64 start, kal_u64 len) { + // Never waits: releasing is not a request another holder can block. + return lock_range(f, start, len, okl::lock_unlock, false); +} + +// --- how much the volume holds ---------------------------------------------- +int kal_fs_capacity(kal_dir d, kal_u64* total, kal_u64* available) { + const int fd = okl::unpack(d.h); + if (fd < 0) return kal_err_invalid; + + okl::kstatfs sf{}; + const okl_long r = okl::sys(okl::nr_fstatfs, fd, reinterpret_cast(&sf)); + if (okl::failed(r)) return okl::translate(r); + + // In bytes, because that is what the interface says and what a caller of it + // wants; this kernel reports blocks and the size of one. + const kal_u64 unit = static_cast(sf.f_bsize); + // ⚠️ `f_bavail' AND NOT `f_bfree'. The second counts blocks the volume has, + // including those only a privileged writer may reach; the first counts the + // ones THIS program could actually use, which is the question asked. + if (total) *total = static_cast(sf.f_blocks) * unit; + if (available) *available = static_cast(sf.f_bavail) * unit; + return kal_ok; +} + // Nodes whose content is another name. int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, const char* target, kal_uintptr target_len, diff --git a/src/process.cpp b/src/process.cpp index 862af72..cad665b 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -200,11 +200,34 @@ int kal_process_spawn(kal_dir base, if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0); if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0); if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0); - // The started program's working directory is the directory supplied - // here, expressed by naming the program relative to it. There is no - // operation that changes a working directory afterwards, because a - // working directory that can be changed is shared mutable state - // between execution contexts. + // ⚠️⚠️ THIS COMMENT USED TO CLAIM A PROPERTY THIS CODE DOES NOT HAVE. + // It said the started program's working directory is the directory + // supplied here. It is not. `b' is the directory the NAME resolves + // against and nothing more --- `execveat' takes a dirfd to resolve + // `p.buf', and resolving a name is not entering a directory. The + // started program's working directory is this implementation's own, + // whatever that happens to be, inherited across the clone above. + // + // ⭐ Found by a consumer's test rather than by reading, which is the + // point: `chdir' then start a program, ask it for its working + // directory, and it answers the directory the caller left --- against + // a host as control, which answers the one the caller entered. + // + // ⇒ AND IT IS NOT FIXABLE HERE. An `fchdir(b)' before the replacement + // would make the sentence true and the behaviour no better: `b' is + // whichever preopen the name resolved under --- for a program named + // `/usr/bin/sh' that is the root --- so the started program would get + // an arbitrary directory instead of a different arbitrary directory. + // Naming the program and naming where it runs are two directories, and + // openkal has an argument for one of them. openkal-musl's `chdir' + // therefore rebinds its own table and cannot do better; the interface + // has no operation that carries the second directory across a spawn. + // + // There is deliberately no operation that changes a working directory + // afterwards, because a working directory that can be changed is + // shared mutable state between execution contexts. That refusal is + // sound and is NOT what is missing --- what is missing is a way to say, + // at the moment of starting, which directory the program starts in. const okl_long why = okl::sys(okl::nr_execveat, b, reinterpret_cast(p.buf), reinterpret_cast(args.slots), @@ -225,6 +248,90 @@ int kal_process_spawn(kal_dir base, return kal_ok; } +// The same, with the started program's lifetime bound to this one's. 0.10. +// +// ⭐⭐ WHY THIS EXISTS, AND IT IS NOT A CONVENIENCE. +// +// A C library asked for `execve' composes it out of what this interface has: +// start the program, wait for it, end with its status. That composition leaves +// THREE images where a system with the operation has two --- the caller, a copy +// that waits, and the program --- and `kal_process_terminate' upon the identifier +// the caller holds reaches the WAITER. Measured with a host as control: identical +// status words, opposite outcomes; the caller is told the program died on the +// signal it sent, while the program runs to completion, unsupervised. +// openkal-linux#13. +// +// ⚠️ THE BINDING IS SET IN THE STARTED IMAGE AND NOT FROM HERE, which is why it +// is a second spawn rather than an operation applied to a handle. This kernel's +// facility answers "end this context when the one that started it ends", and only +// that context can ask for it. +// +// ⚠️ AND IT IS ASKED FOR BEFORE THE REPLACEMENT AND CHECKED AFTER: the setting +// survives the replacement, but the parent could have ended in between --- in +// which case the signal has already been delivered and there is nothing to +// notice. Reading the parent's identity after arming closes that window: if it +// is no longer the one that armed, this image ends now rather than becoming the +// orphan the caller asked not to have. +int kal_process_spawn_bound(kal_dir base, + const char* path, kal_uintptr path_len, + const char** argv, const kal_uintptr* argv_lens, kal_uintptr argc, + const char** envp, const kal_uintptr* envp_lens, kal_uintptr envc, + const kal_spawn_streams* streams, + kal_process* out) { + const int b = okl::unpack(base.h); + if (b < 0 || out == nullptr) return kal_err_invalid; + if (!okl::acceptable(path, path_len)) return kal_err_invalid; + okl::terminated p(path, path_len); + if (!p.ok) return kal_err_invalid; + + vector args, envs; + if (!args.build(argv, argv_lens, argc)) return kal_err_no_memory; + if (!envs.build(envp, envp_lens, envc)) return kal_err_no_memory; + + const okl_long in = streams ? static_cast(streams->in.h) : 0; + const okl_long ou = streams ? static_cast(streams->out.h) : 0; + const okl_long er = streams ? static_cast(streams->err.h) : 0; + + const okl_long mine = okl::sys(okl::nr_getpid); + + exec_report report; + report.open(0); + + const okl_long child = okl::sys(okl::nr_clone, 17 /* SIGCHLD */, 0, 0, 0, 0); + if (okl::failed(child)) { report.close_both(); return okl::translate(child); } + + if (child == 0) { + if (in != 0) okl::sys(okl::nr_dup3, in, 0, 0); + if (ou != 0) okl::sys(okl::nr_dup3, ou, 1, 0); + if (er != 0) okl::sys(okl::nr_dup3, er, 2, 0); + + // 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); + + const okl_long why = + okl::sys(okl::nr_execveat, b, reinterpret_cast(p.buf), + reinterpret_cast(args.slots), + reinterpret_cast(envs.slots), 0); + report.say(why); + okl::sys(okl::nr_exit_group, 127); + for (;;) { } + } + + if (const okl_long why = report.heard()) { + reap(child); + return okl::translate(why); + } + + *out = kal_process{ static_cast(child) }; + return kal_ok; +} + // A channel: a pair of streams of which one end is meant to cross a spawn. // // WHY THIS IS A KERNEL FACILITY AND kal::kit's CHANNEL IS NOT. A started program @@ -389,7 +496,8 @@ void kal_process_close(kal_process) { } kal_uintptr kal_process_props(void) { return KAL_PROCESS_PROP_TERMINATE | KAL_PROCESS_PROP_STREAM_PASSING | KAL_PROCESS_PROP_EXIT_STATUS - | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR; + | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR + | KAL_PROCESS_PROP_BOUND_LIFETIME; } } diff --git a/src/sys.h b/src/sys.h index 0157f8b..98c8a8f 100644 --- a/src/sys.h +++ b/src/sys.h @@ -91,7 +91,7 @@ enum : okl_long { nr_readv = 19, nr_writev = 20, nr_sched_yield = 24, nr_nanosleep = 35, nr_getpid = 39, nr_clone = 56, nr_execve = 59, nr_exit = 60, nr_wait4 = 61, nr_kill = 62, nr_ftruncate = 77, nr_getcwd = 79, nr_fsync = 74, - nr_fcntl = 72, + nr_fcntl = 72, nr_prctl = 157, nr_sched_getaffinity = 204, nr_getppid = 110, nr_arch_prctl = 158, nr_gettid = 186, nr_futex = 202, nr_getdents64 = 217, nr_set_tid_address = 218, nr_clock_gettime = 228, nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234, @@ -180,6 +180,7 @@ enum : okl_long { nr_getpid = 172, nr_mmap = 222, nr_munmap = 215, nr_mprotect = 226, nr_clone = 220, nr_execve = 221, nr_wait4 = 260, nr_renameat = 38, nr_dup3 = 24, nr_execveat = 281, nr_dup2 = -1, nr_fcntl = 25, + nr_prctl = 167, nr_sched_getaffinity = 123, nr_getppid = 173, nr_arch_prctl = -1, nr_utimensat = 88, nr_symlinkat = 36, nr_fstatfs = 44, nr_getrandom = 278, // openkal.net and openkal.datagram @@ -252,6 +253,26 @@ enum : okl_long { // already had there. `dup3' cannot do this: it is told the number, and it // closes what is on it. f_dupfd_cloexec = 1030, + + // ⭐⭐ THE OPEN-FILE FORM AND NOT THE PROCESS FORM, WHICH IS THE WHOLE + // DIFFERENCE. + // + // This kernel's oldest record lock is held by the PROCESS, and it is + // released as soon as that process closes ANY descriptor for the node --- + // so a library that opened one file twice destroyed its own lock, and two + // parts of one program could not exclude each other at all. openkal states + // the holder as the `kal_file', which is exactly what these describe: the + // lock belongs to the open file description and ends when the last + // descriptor for it closes. + f_ofd_getlk = 36, f_ofd_setlk = 37, f_ofd_setlkw = 38, + + // A lock's kind, and where a range begins. + lock_read = 0, lock_write = 1, lock_unlock = 2, + seek_set = 0, + + // "End this context when the one that started it ends", which is what + // binding a started program's lifetime is expressed as here. + pr_set_pdeathsig = 1, #if defined(__x86_64__) o_directory = 0200000, o_nofollow = 0400000, #else @@ -304,6 +325,19 @@ inline bool interrupted(okl_long r) { return r == -e_intr; } // What the kernel reports about the volume a descriptor is on. The layout is // the kernel's own `struct statfs', which is one layout on every architecture // this implementation supports because both are LP64. +// The kernel's own `struct flock'. One layout on both architectures this +// implementation builds for: two shorts, padded to eight, then two 64-bit +// positions and the identifier of a holder this implementation never asks about. +struct kflock { + short l_type; + short l_whence; + int l_pad; + okl_i64 l_start; + okl_i64 l_len; + int l_pid; + int l_pad2; +}; + struct kstatfs { okl_long f_type; okl_long f_bsize; diff --git a/src/task.cpp b/src/task.cpp index 753ca54..c770f3a 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -222,6 +222,32 @@ kal_uintptr kal_task_current(void) { return static_cast(cached); } +// How many contexts can run at the same moment. Version 0.10. +// +// ⚠️⚠️ ADDED BECAUSE ITS ABSENCE WAS A WRONG ANSWER RATHER THAN A REFUSAL. +// `KAL_TASK_PROP_PARALLEL' says WHETHER and not HOW MANY, so a C library above +// had nowhere to look and `hardware_concurrency()' answered 1 with no error --- +// a program sizing a pool of workers got one worker and no way to know. +// Measured: 1 through openkal-musl against 32 on the same machine. +// +// ⭐ THE SET THIS CONTEXT MAY RUN ON, not the set the machine has. A program +// confined to two processors is asked to size itself against two; asking the +// machine would have it size against a number it cannot use. +kal_uintptr kal_task_parallelism(void) { + // The kernel writes a bitmap and reports how many BYTES of it it wrote. + unsigned long mask[128] = { 0 }; // 8192 processors, which is this kernel's own bound + const okl_long n = okl::sys(okl::nr_sched_getaffinity, 0, + static_cast(sizeof mask), + reinterpret_cast(mask)); + if (okl::failed(n) || n <= 0) return 0; // 0 is "cannot say", and is not 1 + + kal_uintptr count = 0; + const okl_long words = n / static_cast(sizeof(unsigned long)); + for (okl_long i = 0; i < words; ++i) + for (unsigned long bit = mask[i]; bit; bit &= bit - 1) ++count; + return count; +} + // The primitive. It is the operation a caller cannot construct: the comparison // and the suspension occur without an intervening opportunity for the value to // change unobserved, and only the environment can arrange that.