diff --git a/README.md b/README.md index f37038d..bfeb694 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.14.0" +openkal = "0.14.1" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.14.0" +openkal-linux = "0.15.1" ``` ## Why it does not use a C library @@ -101,6 +101,29 @@ resolve an absolute path and report one, and a name of `"."` leaves it able to do only the first — which version 0.4 did, and which is why `getcwd` could not have worked above it. +**A started program receives the three streams and its grants, and nothing +else.** Clause 7.13. Every source is first moved above the positions being +filled, so that placing one cannot overwrite another; everything above the +grants is then marked to close on replacement, including what the calling +program itself inherited (`close_range`, from Linux 5.11; `/proc/self/fd` or the +descriptor limit before it). The program is started through a descriptor for its +own file with `O_CLOEXEC`, and only a program that needs an interpreter --- a +`#!` script, or a `binfmt_misc` binary --- keeps that descriptor: the kernel +refuses such a program with `ENOENT` before the point of no return, and the start +is repeated with the flag cleared. Such a script observes `$0` as `/dev/fd/`. + +**Granted directories are named in the environment.** A grant arrives as a +descriptor at 3 and upward, and its name in the variable +`KAL_PREOPENS={;,,}`, the arrangement of systemd's +`LISTEN_FDS` and `LISTEN_FDNAMES`. `` is written by the started process +itself, so a value inherited through a program that does not read it names no +one. A program started with grants enumerates exactly those directories, the +first of which is the directory it regards as the one it was started in; a +program started without them enumerates the working directory and `/`, as +before. A grant names directories to a program that confines itself to its +preopens; it does not confine a program that opens `/` on its own, which is the +environment's responsibility (clause 11, entry 6). + **Interruption is retried, not reported.** A caller cannot distinguish an interrupted call from a genuine failure without knowledge of the platform, and an implementation that reports it produces short transfers on any system that diff --git a/mcpp.toml b/mcpp.toml index 43f740d..90367b0 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.15.0" +version = "0.15.1" description = "The reference implementation of openkal for Linux, written on the kernel's own system-call interface so that it can be placed beneath a C library as well as above one." license = "Apache-2.0" @@ -63,7 +63,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.14.0" +openkal = "0.14.1" # 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 725084c..01ebe71 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -3,6 +3,8 @@ #include #include +namespace okl { extern char** g_envp; } + namespace { @@ -20,11 +22,74 @@ struct preopen { const char* name; kal_uintptr len; okl_uptr handle; }; char g_cwd[4096]; +constexpr kal_uintptr kMaxGrants = 16; + +// THE DIRECTORIES A STARTER GRANTED, WHEN THE PROGRAM WAS STARTED WITH GRANTS. +// +// They arrive as descriptors at 3 and upward, and their names in the variable +// `kal_process_spawn' writes (see `vector::build_env' in process.cpp): +// +// KAL_PREOPENS={;,,} +// +// The value is read only when `' is this process, so one inherited through +// a program that does not read it is not mistaken for a grant. A descriptor that +// is not a directory is reported as a preopen this program may not use, which is +// how an entry that could not be opened is reported anyway. Each descriptor is +// marked to close on replacement as it is taken over, so that a grant reaches +// the program it was made to and not that program's own children (SPEC.md +// clause 7.13). The names point into the environment, which lives as long as +// the program does. +// +// Answers false when there is no such value, which leaves the directories this +// implementation supplies by default. +bool granted(preopen* t, kal_uintptr* n) { + constexpr char key[] = "KAL_PREOPENS="; + 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 false; + + auto number = [&](okl_uptr& out) { + if (*v < '0' || *v > '9') return false; + out = 0; + while (*v >= '0' && *v <= '9') out = out * 10 + static_cast(*v++ - '0'); + return true; + }; + okl_uptr pid = 0; + if (!number(pid) || static_cast(pid) != okl::sys(okl::nr_getpid)) return false; + + kal_uintptr k = 0; + while (*v == ';' && k < kMaxGrants) { + ++v; + okl_uptr fd = 0, len = 0; + if (!number(fd) || *v++ != ',' || !number(len) || *v++ != ',') return false; + for (okl_uptr i = 0; i < len; ++i) if (v[i] == '\0') return false; + + okl::kstat st{}; + const bool dir = !okl::failed(okl::sys(okl::nr_fstat, static_cast(fd), + reinterpret_cast(&st))) + && (st.mode & okl::s_ifmt) == okl::s_ifdir; + if (dir) okl::sys(okl::nr_fcntl, static_cast(fd), okl::f_setfd, okl::fd_cloexec); + t[k++] = { v, len, dir ? okl::pack(static_cast(fd)) : 0u }; + v += len; + } + if (*v != '\0') return false; + *n = k; + return true; +} + preopen* table(kal_uintptr* count) { - static preopen t[2]; + static preopen t[kMaxGrants]; + static kal_uintptr n = 2; static bool opened = false; if (!opened) { opened = true; + if (granted(t, &n)) { if (count) *count = n; return t; } + n = 2; const okl_long n = okl::sys(okl::nr_getcwd, reinterpret_cast(g_cwd), static_cast(sizeof g_cwd)); @@ -45,7 +110,7 @@ preopen* table(kal_uintptr* count) { t[0] = { g_cwd, cwd_len, okl::failed(fd0) ? 0u : okl::pack(static_cast(fd0)) }; t[1] = { "/", 1, okl::failed(fd1) ? 0u : okl::pack(static_cast(fd1)) }; } - if (count) *count = 2; + if (count) *count = n; return t; } diff --git a/src/process.cpp b/src/process.cpp index ab2e174..c3903d2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -8,6 +8,70 @@ namespace { constexpr okl_uptr kMaxEntries = 512; +constexpr char kPreopens[] = "KAL_PREOPENS="; +constexpr okl_uptr kPreopensLen = sizeof kPreopens - 1; +constexpr okl_uptr kPidDigits = 10; + +bool names_preopens(const char* s, kal_uintptr n) { + if (n < kPreopensLen) return false; + for (okl_uptr i = 0; i < kPreopensLen; ++i) + if (s[i] != kPreopens[i]) return false; + return true; +} + +okl_uptr digits(okl_uptr v) { + okl_uptr n = 1; + while (v >= 10) { v /= 10; ++n; } + return n; +} + +char* put_decimal(char* o, okl_uptr v) { + char b[24]; int i = 0; + do { b[i++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (i) *o++ = b[--i]; + return o; +} + +// Every descriptor from `from' upward is marked to close when the image is +// replaced. `close_range' does it in one call from Linux 5.11; before that +// kernel the descriptors the process has are listed from /proc/self/fd, and +// where that is not mounted every number below the descriptor limit is tried. +void close_on_exec_from(okl_long from) { + const okl_long r = okl::sys(okl::nr_close_range, from, static_cast(~0u), + okl::close_range_cloexec); + if (!okl::failed(r)) return; + + const okl_long dir = okl::sys(okl::nr_openat, okl::at_fdcwd, + reinterpret_cast("/proc/self/fd"), + okl::o_rdonly | okl::o_directory | okl::o_cloexec, 0); + if (!okl::failed(dir)) { + alignas(8) char buf[2048]; + for (;;) { + const okl_long n = okl::sys(okl::nr_getdents64, dir, + reinterpret_cast(buf), sizeof buf); + if (n <= 0) break; + for (okl_long at = 0; at < n; ) { + const auto* d = reinterpret_cast(buf + at); + okl_long fd = 0; bool number = d->name[0] != '\0'; + for (const char* c = d->name; *c; ++c) { + if (*c < '0' || *c > '9') { number = false; break; } + fd = fd * 10 + (*c - '0'); + } + if (number && fd >= from && fd != dir) + okl::sys(okl::nr_fcntl, fd, okl::f_setfd, okl::fd_cloexec); + at += d->reclen; + } + } + okl::sys(okl::nr_close, dir); + return; + } + + okl_ulong lim[2] = { 1024, 1024 }; + okl::sys(okl::nr_prlimit64, 0, okl::rlimit_nofile, 0, reinterpret_cast(lim)); + for (okl_long fd = from; fd < static_cast(lim[0]); ++fd) + okl::sys(okl::nr_fcntl, fd, okl::f_setfd, okl::fd_cloexec); +} + // The counted arrays the interface takes become the terminated arrays the // kernel takes. Every allocation happens before the program is duplicated, so // that the duplicate performs nothing but two system calls: a duplicate of a @@ -40,6 +104,82 @@ struct vector { return true; } + // THE ENVIRONMENT, WHICH CARRIES THE NAMES OF THE GRANTED DIRECTORIES. + // + // A granted directory reaches the started program as a descriptor, and a + // descriptor carries no name. The names travel in one variable, + // + // KAL_PREOPENS={;,,} + // + // which the started program reads when it first enumerates its preopens + // (`table' in fs.cpp). It is the arrangement this environment already uses + // to hand a started program named descriptors --- systemd's LISTEN_FDS, + // LISTEN_FDNAMES and LISTEN_PID --- and it is bound to the process it was + // written for in the same way: `' is ten digits written by the + // duplicate once it knows its own number, so a value inherited through a + // program that does not read it, a shell for example, names no one when it + // arrives one generation further down. + // + // Every variable of that name the caller supplied is left out, so a value + // is never forwarded; one is added only when the caller asked for grants, + // and asking for none (a count of zero) is a value with no entries. + char* pid_digits = nullptr; + + bool build_env(const char** items, const kal_uintptr* lens, kal_uintptr n, + const kal_preopen* grants, kal_uintptr g) { + if (n > kMaxEntries) { ok = false; return false; } + kal_uintptr kept = 0; + okl_uptr total = 0; + for (kal_uintptr i = 0; i < n; ++i) { + if (names_preopens(items[i], lens[i])) continue; + ++kept; total += lens[i] + 1; + } + okl_uptr extra = 0; + if (grants) { + extra = kPreopensLen + kPidDigits + 1; + 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*); + 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; + okl::copy(bytes + at, items[i], lens[i]); + bytes[at + lens[i]] = '\0'; + slots[k++] = bytes + at; + at += lens[i] + 1; + } + if (grants) { + char* o = bytes + at; + slots[k++] = o; + okl::copy(o, kPreopens, kPreopensLen); o += kPreopensLen; + pid_digits = o; + okl::fill(o, '0', kPidDigits); o += kPidDigits; + for (kal_uintptr i = 0; i < g; ++i) { + *o++ = ';'; o = put_decimal(o, 3 + i); + *o++ = ','; o = put_decimal(o, grants[i].len); + *o++ = ','; + okl::copy(o, grants[i].name, grants[i].len); o += grants[i].len; + } + *o = '\0'; + } + slots[k] = nullptr; + return true; + } + + // 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; + } + } + ~vector() { if (slots) kal_free(slots, slots_bytes, alignof(char*)); if (bytes) kal_free(bytes, bytes_bytes, 1); @@ -192,20 +332,27 @@ int kal_process_spawn(const kal_spawn* how, 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; - // Resolved before the duplication, because a failure after it would leave a - // child to be reaped and a caller with an error it cannot act upon. + // child to be reaped and a caller with an error it cannot act upon. A name + // travels in the environment, which cannot carry a zero byte. constexpr kal_uintptr max_grants = 16; if (how->grant_count > max_grants) return kal_err_invalid; - int granted[max_grants]; + okl_long granted[max_grants]; for (kal_uintptr i = 0; i < how->grant_count; ++i) { - granted[i] = okl::unpack(how->grants[i].dir.h); - if (granted[i] < 0) return kal_err_invalid; + const kal_preopen& g = how->grants[i]; + const int fd = okl::unpack(g.dir.h); + if (fd < 0) return kal_err_invalid; + if (g.len > okl::max_name || (g.len > 0 && g.name == nullptr)) return kal_err_invalid; + for (kal_uintptr c = 0; c < g.len; ++c) + if (g.name[c] == '\0') return kal_err_invalid; + granted[i] = fd; } + 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)) + 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; @@ -229,24 +376,54 @@ int kal_process_spawn(const kal_spawn* how, 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); - - // dup3 REFUSES A DUPLICATION ONTO ITSELF, which the ordinary case - // reaches whenever a granted directory already occupies the number it - // is destined for. Refusing there is correct of dup3 --- the flags could - // not be applied --- and here it means the descriptor is already in - // place, so it is left alone rather than treated as a failure. - for (kal_uintptr i = 0; i < how->grant_count; ++i) { - const okl_long want = static_cast(3 + i); - if (granted[i] != want) - okl::sys(okl::nr_dup3, granted[i], want, 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. // - // `execveat' below takes `b' as a dirfd, but that only RESOLVES the + // `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 @@ -255,7 +432,7 @@ int kal_process_spawn(const kal_spawn* how, // 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, w); okl::failed(e)) { + 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 (;;) { } @@ -279,8 +456,9 @@ int kal_process_spawn(const kal_spawn* how, okl::sys(okl::nr_exit_group, 127); } - // THE BASE IS DUPLICATED SO THAT IT SURVIVES THE REPLACEMENT, AND - // WITHOUT THIS A WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED AT ALL. + // 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 @@ -289,13 +467,13 @@ int kal_process_spawn(const kal_spawn* how, // 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 dirfd is gone. 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: // - // dirfd WITH O_CLOEXEC execveat -> ENOENT - // dirfd WITHOUT O_CLOEXEC STARTED ok + // 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 @@ -303,17 +481,61 @@ int kal_process_spawn(const kal_spawn* how, // It reproduces natively on x86_64 with a `#!' script, which is what a // consumer meets on any machine. // - // Duplicated 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. `dup' clears - // the flag by definition, so the copy is the exec-visible one. - const okl_long visible = okl::sys(okl::nr_fcntl, b, okl::f_dupfd, 0); - const okl_long base = okl::failed(visible) ? b : visible; - - const okl_long why = - okl::sys(okl::nr_execveat, base, reinterpret_cast(p.buf), - reinterpret_cast(args.slots), - reinterpret_cast(envs.slots), 0); + // 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. + // + // What is given up: a script now sees `$0' as `/dev/fd/' and not as + // a path under its directory, so one that locates its neighbours with + // `dirname "$0"' no longer finds them. That is the price of leaving a + // descriptor for the file and not for the directory, the same one + // `fexecve' charges. The descriptor still names the program: it 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. + 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) { + okl::sys(okl::nr_fcntl, exe, okl::f_setfd, 0); + why = start(); + } // Reached only when the replacement did not happen, because when it does // there is nothing here to reach. report.say(why); diff --git a/src/sys.h b/src/sys.h index 0c07fd8..0c7f819 100644 --- a/src/sys.h +++ b/src/sys.h @@ -97,6 +97,7 @@ enum : okl_long { nr_clock_getres = 229, nr_exit_group = 231, nr_tgkill = 234, nr_openat = 257, nr_mkdirat = 258, nr_newfstatat = 262, nr_unlinkat = 263, nr_renameat = 264, nr_readlinkat = 267, nr_dup3 = 292, nr_execveat = 322, + nr_close_range = 436, nr_prlimit64 = 302, nr_dup2 = 33, nr_utimensat = 280, nr_symlinkat = 266, nr_fstatfs = 138, nr_getrandom = 318, // openkal 0.13: whether a node may be started @@ -183,7 +184,8 @@ enum : okl_long { nr_sched_yield = 124, nr_kill = 129, nr_tgkill = 131, nr_gettid = 178, 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_dup3 = 24, nr_execveat = 281, + nr_close_range = 436, nr_prlimit64 = 261, 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, @@ -256,6 +258,14 @@ enum : okl_long { o_creat = 0100, o_excl = 0200, o_trunc = 01000, o_append = 02000, o_cloexec = 02000000, + // A descriptor that only NAMES a node, and the empty name `execveat' accepts + // for it. See the start in `kal_process_spawn'. + o_path = 010000000, at_empty_path = 0x1000, + + // The descriptor flag, set and cleared one descriptor at a time, and the + // form of `close_range' that sets it on every descriptor of a range. + f_setfd = 2, fd_cloexec = 1, close_range_cloexec = 4, rlimit_nofile = 7, + // THE LOWEST FREE DESCRIPTOR AT OR ABOVE A BOUND, which is the one // primitive that moves a descriptor out of the way WITHOUT NAMING the // number it moves to --- and therefore without closing whatever a caller @@ -263,13 +273,6 @@ enum : okl_long { // closes what is on it. f_dupfd_cloexec = 1030, - // The same primitive WITHOUT the flag, which is the point of having both. - // A descriptor duplicated this way survives a replacement, and starting a - // program that needs an interpreter depends on exactly that --- see the - // duplication in `kal_process_spawn'. `dup' would do as well and this - // architecture pair does not agree on whether it exists. - f_dupfd = 0, - // THE OPEN-FILE FORM AND NOT THE PROCESS FORM, WHICH IS THE WHOLE // DIFFERENCE. // diff --git a/tests/conformance_additions.cpp b/tests/conformance_additions.cpp index 1f459cc..e5aef1e 100644 --- a/tests/conformance_additions.cpp +++ b/tests/conformance_additions.cpp @@ -181,6 +181,37 @@ int main() { kal_fs_remove(here(), prog, pn); } + // A started `#!' script is handed `/dev/fd/' by the kernel and opens it + // after the replacement, so a descriptor has to be left behind for it. It + // must be one for the script and not for the directory the name was found + // in: a program that starts other programs under a sandbox passes every + // descriptor on, and a directory among them reaches outside it. The base + // and the working directory are the same here, so it is `.' that must not + // be among them. + { + const char* prog = "okl-fd-probe.tmp"; + const kal_uintptr pn = std::strlen(prog); + check(put(prog, "#!/bin/sh\nfor f in /proc/self/fd/*; do\n" + " [ \"$f\" -ef . ] && exit 1\ndone\nexit 0\n"), + "a script is written"); + check(kal_fs_set_executable_at(here(), prog, pn, 1) == kal_ok, + "the script is recorded as startable"); + + kal_process p{}; + const char* argv[1] = { prog }; + const kal_uintptr lens[1] = { pn }; + const kal_spawn how{ here(), here(), nullptr, nullptr, 0, 0 }; + check(kal_process_spawn(&how, prog, pn, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p) == kal_ok, + "a `#!' script is started"); + int status = -1, terminated = -1; + kal_process_wait(p, &status, &terminated); + check(terminated == 0 && status == 0, + "the script ran, and it inherited no descriptor for its base directory"); + kal_process_close(p); + kal_fs_remove(here(), prog, pn); + } + std::printf("openkal-linux: the operations version 0.5 added\n"); return failures == 0 ? 0 : 1; } diff --git a/tests/conformance_spawn.cpp b/tests/conformance_spawn.cpp new file mode 100644 index 0000000..3260a2e --- /dev/null +++ b/tests/conformance_spawn.cpp @@ -0,0 +1,226 @@ +// What a started program receives: the three streams and the directories it was +// granted, and nothing else (SPEC.md clause 7.13). +// +// Each case starts a program and has the PROGRAM report what it received, since +// the claim is about the started image and not about the caller. Two programs +// are used. `/bin/sh' is an ordinary executable that needs nothing to survive +// the start, so any descriptor above 2 it holds was conveyed by the start. This +// test itself, started again with `--grant-child', reads its preopens through +// `kal_fs_preopen', which is how a granted directory is defined to arrive. +#include +#include +#include +#include +import openkal.fs; +import openkal.stream; +import openkal.types; +import openkal.process; + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +const char kMarker[] = "okl-grant-marker"; + +kal_dir working() { return kal::fs::working(); } + +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{}; +} + +bool write_marker(kal_dir d, const char* text) { + kal_file f{}; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate; + if (kal::fs::open_file(d, kMarker, sizeof kMarker - 1, flags, &f) != kal_ok) return false; + const kal_intptr r = kal_stream_write(kal_fs_stream(f), text, std::strlen(text)); + kal_fs_close_file(f); + return r == static_cast(std::strlen(text)); +} + +// --- the started side -------------------------------------------------------- +// +// argv: --grant-child { } +// is the content of the marker file in that directory, or `@' +// for a directory entry that must exist there. The status names the first +// disagreement, so that a failure in the caller says which one it was. +int grant_child(int argc, char** argv) { + const kal_uintptr n = kal_fs_preopen_count(); + if (std::strcmp(argv[2], "ambient") == 0) { + kal_dir d{}; char nm[8]; kal_uintptr l = 0; + if (n != 2) return 10; + if (kal_fs_preopen(1, &d, nm, sizeof nm, &l) != kal_ok || l != 1 || nm[0] != '/') return 11; + return 0; + } + const kal_uintptr want = static_cast(std::atoi(argv[2])); + if (n != want) return 20; + for (kal_uintptr i = 0; i < want; ++i) { + const char* name = argv[3 + 2 * i]; + const char* expect = argv[4 + 2 * i]; + kal_dir d{}; char nm[256]; kal_uintptr l = 0; + if (kal_fs_preopen(i, &d, nm, sizeof nm, &l) != kal_ok) return 30 + static_cast(i); + if (l != std::strlen(name) || std::memcmp(nm, name, l) != 0) return 40 + static_cast(i); + if (expect[0] == '@') { + kal_node_info info = kal::fs::info_for_caller(); + if (kal_fs_info(d, expect + 1, std::strlen(expect + 1), kal::fs::field::kind, 0, &info) != kal_ok + || info.kind != kal_node_directory) + return 50 + static_cast(i); + } else { + kal_file f{}; char buf[64]; + if (kal::fs::open_file(d, kMarker, sizeof kMarker - 1, kal::fs::open::read, &f) != kal_ok) + return 60 + static_cast(i); + const kal_intptr r = kal_stream_read(kal_fs_stream(f), buf, sizeof buf); + kal_fs_close_file(f); + if (r != static_cast(std::strlen(expect)) || std::memcmp(buf, expect, r) != 0) + return 70 + static_cast(i); + } + } + return 0; +} + +// --- the starting side ------------------------------------------------------- + +char g_self[1024]; + +// Starts this test again under the root preopen and answers the status it +// finished with, or -1 when the start itself was refused. +int run_child(const kal_preopen* grants, kal_uintptr count, + const char* const* args, kal_uintptr nargs, + const char** envp = nullptr, kal_uintptr envc = 0) { + const char* argv[16] = { "conformance_spawn", "--grant-child" }; + kal_uintptr lens[16] = { 17, 13 }; + for (kal_uintptr i = 0; i < nargs; ++i) { argv[2 + i] = args[i]; lens[2 + i] = std::strlen(args[i]); } + kal_uintptr elens[4] = {}; + for (kal_uintptr i = 0; i < envc; ++i) elens[i] = std::strlen(envp[i]); + const kal_spawn how{ root(), working(), nullptr, grants, count, 0 }; + kal_process p{}; + const char* rel = g_self + 1; + if (kal_process_spawn(&how, rel, std::strlen(rel), argv, lens, 2 + nargs, + envp, elens, envc, nullptr, &p) != kal_ok) + return -1; + int status = -1, terminated = -1; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); + return terminated == 0 ? status : -1; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc > 2 && std::strcmp(argv[1], "--grant-child") == 0) return grant_child(argc, argv); + + const ssize_t sl = readlink("/proc/self/exe", g_self, sizeof g_self - 1); + check(sl > 1 && g_self[0] == '/', "the test can name its own program"); + g_self[sl > 0 ? sl : 0] = '\0'; + check(root().h != 0, "a directory covering the file system is supplied"); + + // Two directories with distinct contents, so that a grant arriving under + // the wrong name is told apart from one arriving under the right name. + const char* da = "okl-grant-a.tmp"; + const char* db = "okl-grant-b.tmp"; + kal_fs_mkdir(working(), da, std::strlen(da)); + kal_fs_mkdir(working(), db, std::strlen(db)); + kal_dir a{}, b{}; + check(kal_fs_open_dir(working(), da, std::strlen(da), &a) == kal_ok + && kal_fs_open_dir(working(), db, std::strlen(db), &b) == kal_ok, + "two directories are made"); + check(write_marker(a, "A") && write_marker(b, "B"), "each holds its own marker"); + + // Named grants arrive in order, under their names, as the directories named. + // The second name holds both separators the variable uses. + { + const kal_preopen g[2] = { { a, "/first", 6 }, { b, "b;2,x", 5 } }; + const char* args[] = { "2", "/first", "A", "b;2,x", "B" }; + check(run_child(g, 2, args, 5) == 0, + "granted directories arrive in order, under their names, as the directories named"); + } + { + const kal_preopen g[2] = { { b, "/b", 2 }, { a, "/a", 2 } }; + const char* args[] = { "2", "/b", "B", "/a", "A" }; + check(run_child(g, 2, args, 5) == 0, "and in the other order"); + } + + // The caller's own preopens occupy the numbers grants are placed at. Placing + // one at a time where it stood made the second of these a copy of the first, + // and made the first, already at its own position, never arrive at all. + { + const kal_preopen g[2] = { { root(), "/", 1 }, { working(), "/work", 5 } }; + const char* args[] = { "2", "/", "@proc", "/work", "@okl-grant-a.tmp" }; + check(run_child(g, 2, args, 5) == 0, + "grants whose sources occupy each other's positions arrive as themselves"); + const kal_preopen h[2] = { { working(), "/work", 5 }, { root(), "/", 1 } }; + const char* hargs[] = { "2", "/work", "@okl-grant-a.tmp", "/", "@proc" }; + check(run_child(h, 2, hargs, 5) == 0, "and in the other order"); + } + + // A count of zero is a request for no preopens, which is different from not + // asking. Not asking leaves what the implementation supplies by default. + { + const kal_preopen none[1] = {}; + const char* zero[] = { "0" }; + check(run_child(none, 0, zero, 1) == 0, "a count of zero starts a program with no preopens"); + const char* ambient[] = { "ambient" }; + check(run_child(nullptr, 0, ambient, 1) == 0, + "not asking leaves the directories supplied by default"); + } + + // A value of the variable the caller supplies is not forwarded, whether or + // not it names the process: it is the implementation's to write. + { + const char* env[] = { "KAL_PREOPENS=0000000001;3,1,/" }; + const char* ambient[] = { "ambient" }; + check(run_child(nullptr, 0, ambient, 1, env, 1) == 0, + "a variable the caller supplies is not taken for a grant"); + } + + // A name travels in the environment, which cannot carry a zero byte. + { + const kal_preopen g[1] = { { a, "x\0y", 3 } }; + const char* args[] = { "1", "x", "A" }; + check(run_child(g, 1, args, 3) == -1, "a name holding a zero byte is refused"); + } + + // An ordinary executable needs nothing to survive the start, so it receives + // nothing above the three streams --- including what this process itself + // inherited. The glob is expanded before the loop, so the descriptor the + // shell had open to list the directory is gone by the time it is tested. + { + const char* body = "for f in /proc/self/fd/*; do [ -e \"$f\" ] || continue; " + "[ \"${f##*/}\" -gt 2 ] && { echo \"inherited $f -> $(readlink $f)\" >&2; exit 1; }; " + "done; exit 0"; + const char* sargv[3] = { "sh", "-c", body }; + const kal_uintptr slens[3] = { 2, 2, std::strlen(body) }; + const char* paths[2] = { "bin/sh", "usr/bin/sh" }; + const kal_spawn how{ root(), working(), nullptr, nullptr, 0, 0 }; + kal_process p{}; + int rc = kal_err_invalid; + for (int i = 0; i < 2 && rc != kal_ok; ++i) + rc = kal_process_spawn(&how, paths[i], std::strlen(paths[i]), sargv, slens, 3, + nullptr, nullptr, 0, nullptr, &p); + check(rc == kal_ok, "an ordinary executable is started"); + if (rc == kal_ok) { + int status = -1, terminated = -1; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); + check(terminated == 0 && status == 0, + "an ordinary executable receives no descriptor above the three streams"); + } + } + + kal_fs_remove(a, kMarker, sizeof kMarker - 1); + kal_fs_remove(b, kMarker, sizeof kMarker - 1); + kal_fs_close_dir(a); + kal_fs_close_dir(b); + kal_fs_remove(working(), da, std::strlen(da)); + kal_fs_remove(working(), db, std::strlen(db)); + + std::printf("openkal-linux: what a started program receives\n"); + return failures == 0 ? 0 : 1; +}