diff --git a/README.md b/README.md index b4f4ea3..326ded8 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.16.0" +openkal = "0.17.0" [target.'cfg(os = "linux")'.dependencies] -openkal-linux = "0.17.0" +openkal-linux = "0.18.0" ``` ## Why it does not use a C library diff --git a/mcpp.toml b/mcpp.toml index d70fb84..fd3961b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-linux" -version = "0.17.0" +version = "0.18.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" @@ -63,7 +63,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.16.0" +openkal = "0.17.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/endpoint.h b/src/endpoint.h index 37ad624..1a64148 100644 --- a/src/endpoint.h +++ b/src/endpoint.h @@ -98,6 +98,9 @@ inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) { return kal_ok; } + // A connection made at a name (0.17): it has no address and port to state. + if (in.family == 1 /* AF_UNIX */) return kal_err_not_supported; + return kal_err_invalid; } diff --git a/src/env.cpp b/src/env.cpp index 90dd6ed..71790ac 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -15,6 +15,9 @@ namespace okl { int g_argc = 0; char** g_argv = nullptr; char** g_envp = nullptr; +// Whether this program was started with SIGPIPE ignored: what a program it +// starts is given back (process.cpp). +bool g_pipe_started_ignored = false; okl_ulong* g_auxv = nullptr; void record(int argc, char** argv, char** envp) { @@ -124,19 +127,35 @@ bool recover(char*** argv_out, int* argc_out, char*** envp_out) { // NOT A POLICY CHOICE ABOUT SIGNALS IN GENERAL. This is the one signal an // ordinary openkal operation provokes; the rest are left exactly as this program // was started with. +// +// AND IT IS THIS PROGRAM'S, NOT PASSED ON. An ignored disposition survives +// `execve', so every program started from here would otherwise begin with +// SIGPIPE ignored --- `yes | head' inside a started shell then reports a write +// error instead of ending, and a shell cannot undo an ignore it was started +// with. What was found here is recorded, and `kal_process_start' gives a +// started program the disposition this one was started with (process.cpp). [[gnu::constructor(101)]] void quiet_the_signal_openkal_cannot_report() { // struct k_sigaction as this kernel takes it: handler, flags, restorer, mask. - struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } - ignore{ reinterpret_cast(1) /* SIG_IGN */, 0, nullptr, 0 }; - okl::sys(okl::nr_rt_sigaction, 13 /* SIGPIPE */, - reinterpret_cast(&ignore), 0, sizeof ignore.mask); + struct k_sigaction { void* handler; unsigned long flags; void* restorer; unsigned long mask; }; + k_sigaction ignore{ reinterpret_cast(1) /* SIG_IGN */, 0, nullptr, 0 }; + k_sigaction found{}; + if (!okl::failed(okl::sys(okl::nr_rt_sigaction, 13 /* SIGPIPE */, + reinterpret_cast(&ignore), + reinterpret_cast(&found), sizeof ignore.mask))) + okl::g_pipe_started_ignored = found.handler == reinterpret_cast(1); } +// THE PUBLISHED HANDLE FIRST, THE REGISTERS ONLY WHERE THERE IS NONE. The +// check above tells a shifted vector from a real one, not every set of +// register contents from one: a static program over musl (an ok image's M3 +// program) passed it with whatever the registers held, and its image saw no +// environment at all. `environ' is the C library's own record of the vectors +// it was started with, so it is read wherever the library has one. [[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) { if (okl::g_argv != nullptr) return; - if (plausible(argc, argv, envp)) { okl::record(argc, argv, envp); return; } char** rargv = nullptr; char** renvp = nullptr; int rargc = 0; - if (recover(&rargv, &rargc, &renvp)) okl::record(rargc, rargv, renvp); + if (recover(&rargv, &rargc, &renvp)) { okl::record(rargc, rargv, renvp); return; } + if (plausible(argc, argv, envp)) okl::record(argc, argv, envp); } } // namespace @@ -202,4 +221,35 @@ kal_intptr kal_env_var_at(kal_uintptr index, char* out, kal_uintptr cap) { return -kal_err_not_found; } +// THE IMAGE THE KERNEL STARTED (0.17). `/proc/self/exe' names the file the +// kernel mapped, resolved; where the pseudo-file system is not mounted, the +// name the kernel was given (AT_EXECFN), which is the starter's spelling and +// may be relative to where the starter was --- the environment's answer +// either way, which is what the declaration asks for. +// A program built for its environment was loaded by nothing the interface +// names (clause 12: only an image has a loader to name). +kal_intptr kal_env_loader(char* out, kal_uintptr cap) { + (void)out; (void)cap; + return -kal_err_not_found; +} + +kal_intptr kal_env_image(char* out, kal_uintptr cap) { + char buf[4096]; + const okl_long n = okl::sys(okl::nr_readlinkat, okl::at_fdcwd, + reinterpret_cast("/proc/self/exe"), + reinterpret_cast(buf), sizeof buf); + if (!okl::failed(n) && n > 0 && n < static_cast(sizeof buf)) + return give(buf, static_cast(n), out, cap); + if (const okl_ulong fn = okl::auxval(31 /* AT_EXECFN */); fn != 0) { + const char* name = reinterpret_cast(fn); + return give(name, okl::length(name), out, cap); + } + return -kal_err_not_found; +} + +// For conventions only, as the declaration restricts it. +kal_intptr kal_env_system(char* out, kal_uintptr cap) { + return give("linux", 5, out, cap); +} + } diff --git a/src/exec.cpp b/src/exec.cpp index eb2f8fa..d04a639 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -1,6 +1,8 @@ #include "sys.h" #include +namespace okl { okl_ulong auxval(okl_ulong key); } + // openkal.exec on this kernel. // // A mapping obtained writable and made executable afterwards. The kernel places @@ -18,7 +20,18 @@ namespace { -constexpr okl_uptr kPage = 4096; +// The kernel's page, as it told this program at inception (AT_PAGESZ). Four +// kibibytes was written here, and an aarch64 kernel built for sixteen or +// sixty-four rounds every protection to its own page, so a part published at a +// four-kibibyte boundary would have taken its neighbour with it. +okl_uptr page() { + static okl_uptr p = 0; + if (p == 0) { + const okl_ulong a = okl::auxval(6 /* AT_PAGESZ */); + p = (a != 0 && (a & (a - 1)) == 0) ? static_cast(a) : 4096; + } + return p; +} okl_uptr round_up(okl_uptr n, okl_uptr to) { return (n + to - 1) & ~(to - 1); } @@ -28,7 +41,7 @@ extern "C" { void* kal_exec_alloc(kal_uintptr size) { if (size == 0) return nullptr; - const okl_uptr bytes = round_up(static_cast(size), kPage); + const okl_uptr bytes = round_up(static_cast(size), page()); const okl_long r = okl::sys(okl::nr_mmap, 0, static_cast(bytes), okl::prot_read | okl::prot_write, okl::map_private | okl::map_anonymous, -1, 0); @@ -38,7 +51,7 @@ void* kal_exec_alloc(kal_uintptr size) { int kal_exec_publish(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return kal_err_invalid; - const okl_uptr bytes = round_up(static_cast(size), kPage); + const okl_uptr bytes = round_up(static_cast(size), page()); const okl_long r = okl::sys(okl::nr_mprotect, reinterpret_cast(p), static_cast(bytes), okl::prot_read | okl::prot_exec); @@ -85,9 +98,25 @@ int kal_exec_publish(void* p, kal_uintptr size) { return kal_ok; } +// PART OF A REGION (0.17): the kernel protects per page, so a part is a run of +// whole pages, and a part that is not is refused rather than rounded --- a +// rounded part would take writable data beside it out of reach. +int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { + const okl_uptr g = page(); + if (region == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 + || (reinterpret_cast(region) & (g - 1)) != 0) + return kal_err_invalid; + const okl_long r = okl::sys(okl::nr_mprotect, + reinterpret_cast(static_cast(region) + offset), + static_cast(size), okl::prot_read | okl::prot_exec); + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + +kal_uintptr kal_exec_granularity(void) { return page(); } + void kal_exec_free(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return; - const okl_uptr bytes = round_up(static_cast(size), kPage); + const okl_uptr bytes = round_up(static_cast(size), page()); okl::sys(okl::nr_munmap, reinterpret_cast(p), static_cast(bytes)); } diff --git a/src/fs.cpp b/src/fs.cpp index ddc5086..a07d36a 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -135,6 +135,13 @@ kal_uintptr volume_props(int fd) { const kal_uintptr conservative = KAL_FS_PROP_MODIFIED_TIME | KAL_FS_PROP_ATOMIC_RENAME | KAL_FS_PROP_LOCKS | KAL_FS_PROP_CAPACITY; + // A NAME HELD OPEN IS REMOVED AT ONCE (0.17, clause 7.14) on every format + // whose names the VFS itself keeps: `unlink' drops the entry and the inode + // lives on until its last descriptor closes. Not in the conservative set, + // because a format the VFS does not keep may not: a network volume's client + // renames a held file to a hidden name and removes it at the close, and a + // user-space format may do the same, so the directory is not empty. + const kal_uintptr local = conservative | KAL_FS_PROP_REMOVE_OPEN; if (fd < 0) return 0; okl::kstatfs sf{}; @@ -143,12 +150,23 @@ kal_uintptr volume_props(int fd) { switch (sf.f_type) { // Formats with a case-sensitive namespace and nodes that name others. + // + // A rename that does not replace (0.17) is `renameat2' with + // RENAME_NOREPLACE, which each of these formats implements except the + // one whose support arrived late and is not known from its type; a + // socket bound to a name (0.17, KAL_FS_PROP_LOCAL_ENDPOINT) is a node + // every one of them stores. case okl::fs_ext234: case okl::fs_btrfs: case okl::fs_xfs: case okl::fs_f2fs: case okl::fs_tmpfs: case okl::fs_overlay: - case okl::fs_zfs: case okl::fs_bcachefs: - return conservative | KAL_FS_PROP_CASE_SENSITIVE + case okl::fs_bcachefs: + return local | KAL_FS_PROP_CASE_SENSITIVE | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS - | KAL_FS_PROP_EXECUTABLE; + | KAL_FS_PROP_EXECUTABLE + | KAL_FS_PROP_RENAME_NO_REPLACE | KAL_FS_PROP_LOCAL_ENDPOINT; + case okl::fs_zfs: + return local | KAL_FS_PROP_CASE_SENSITIVE + | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS + | KAL_FS_PROP_EXECUTABLE | KAL_FS_PROP_LOCAL_ENDPOINT; // Read-only formats: the nodes are there and none can be made, and a // rename cannot be atomic because there is no rename. @@ -164,14 +182,14 @@ kal_uintptr volume_props(int fd) { // names another. `symlink' on such a volume reports EPERM, and this is // where a caller learns that before it tries. case okl::fs_msdos: case okl::fs_exfat: - return conservative; + return local; // A case-insensitive namespace, with nodes that name others. case okl::fs_ntfs: case okl::fs_ntfs3: - return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; + return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS; // The same, on a format that stores a mode. case okl::fs_hfsplus: - return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS + return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS | KAL_FS_PROP_EXECUTABLE; default: @@ -179,6 +197,16 @@ kal_uintptr volume_props(int fd) { } } +} // namespace (reopened below) + +namespace okl { +// The volume's properties, for the interfaces outside this file that ask one +// (openkal.net's rendezvous at a name). +kal_uintptr volume_props_of(int fd) { return volume_props(fd); } +} // namespace okl + +namespace { + // Writes no more of the structure than the caller says exists on its side, and // reports which fields it filled. // @@ -594,6 +622,27 @@ int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, return okl::failed(r) ? okl::translate(r) : kal_ok; } +// A RENAME THAT DOES NOT REPLACE (0.17) is the kernel's own: `renameat2' with +// RENAME_NOREPLACE tests and renames in one act. A format that does not +// implement the flag answers EINVAL, and a kernel before 3.15 ENOSYS; both are +// a volume that cannot, which `kal_fs_props' answers before the call for the +// formats this file knows. +int kal_fs_rename_flags(kal_dir from, const char* a, kal_uintptr alen, + kal_dir to, const char* b, kal_uintptr blen, + kal_uintptr flags) { + if ((flags & ~static_cast(KAL_RENAME_NO_REPLACE)) != 0) return kal_err_invalid; + if (flags == 0) return kal_fs_rename(from, a, alen, to, b, blen); + const int f = okl::unpack(from.h), t2 = okl::unpack(to.h); + if (f < 0 || t2 < 0 || !okl::acceptable(a, alen) || !okl::acceptable(b, blen)) return kal_err_invalid; + if ((volume_props(t2) & KAL_FS_PROP_RENAME_NO_REPLACE) == 0) return kal_err_not_supported; + okl::terminated ta(a, alen), tb(b, blen); + if (!ta.ok || !tb.ok) return kal_err_invalid; + const okl_long r = okl::sys(okl::nr_renameat2, f, reinterpret_cast(ta.buf), + t2, reinterpret_cast(tb.buf), 1 /* RENAME_NOREPLACE */); + if (r == -okl::e_inval || r == -okl::e_nosys) return kal_err_not_supported; + return okl::failed(r) ? okl::translate(r) : kal_ok; +} + // Enumeration holds a descriptor of its own, obtained by opening the directory // through itself. A duplicate would share the file offset with the handle the // caller holds, so two enumerations of one directory would consume each diff --git a/src/net.cpp b/src/net.cpp index 1f4e56d..f115e62 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3,6 +3,8 @@ #include "endpoint.h" #include +namespace okl { kal_uintptr volume_props_of(int fd); } // fs.cpp + // openkal.net upon the kernel's socket calls. // // A CONNECTION IS AN OWNED HANDLE AND THE STREAM IS BORROWED FROM IT, exactly as @@ -127,6 +129,79 @@ int kal_net_accept(kal_net_listener l, kal_net_conn* out) { } } +// A RENDEZVOUS AT A NAME (0.17): a socket of the local family bound to a name +// in the directory. The kernel binds to a path and not beneath a descriptor, +// so the directory is named through the descriptor itself, +// `/proc/self/fd//', which the kernel resolves through the +// descriptor --- the directory the caller holds, wherever it has since been +// moved. The address holds 108 bytes, and a name that does not fit beside +// that prefix is one this environment cannot place (kal_err_invalid). +static int local_address(kal_dir base, const char* name, kal_uintptr len, + unsigned char (&addr)[110], okl_long& alen) { + const int dir = okl::unpack(base.h); + if (dir < 0 || !okl::acceptable(name, len) || len == 0) return kal_err_invalid; + for (kal_uintptr i = 0; i < len; ++i) if (name[i] == '/') return kal_err_invalid; + if ((okl::volume_props_of(dir) & KAL_FS_PROP_LOCAL_ENDPOINT) == 0) return kal_err_not_supported; + char path[108]; + const char prefix[] = "/proc/self/fd/"; + okl_uptr at = 0; + for (okl_uptr i = 0; i + 1 < sizeof prefix; ++i) path[at++] = prefix[i]; + char d[12]; int n = 0; + for (unsigned v = static_cast(dir); ; v /= 10) { d[n++] = static_cast('0' + v % 10); if (v < 10) break; } + while (n) path[at++] = d[--n]; + path[at++] = '/'; + if (at + len + 1 > sizeof path) return kal_err_invalid; + for (kal_uintptr i = 0; i < len; ++i) path[at++] = name[i]; + path[at] = '\0'; + addr[0] = 1; addr[1] = 0; // sa_family = AF_UNIX + for (okl_uptr i = 0; i <= at; ++i) addr[2 + i] = static_cast(path[i]); + alen = static_cast(2 + at + 1); + return kal_ok; +} + +int kal_net_listen_at(kal_dir base, const char* name, kal_uintptr len, kal_net_listener* out) { + if (out == nullptr) return kal_err_invalid; + unsigned char addr[110] = {}; + okl_long alen = 0; + if (const int rc = local_address(base, name, len, addr, alen); rc != kal_ok) return rc; + const okl_long fd = okl::sys(okl::nr_socket, 1 /* AF_UNIX */, okl::sock_stream | okl::sock_cloexec, 0); + if (okl::failed(fd)) return okl::translate(fd); + if (const okl_long r = okl::sys(okl::nr_bind, fd, reinterpret_cast(addr), alen); okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return r == -98 /* EADDRINUSE */ ? kal_err_exists : okl::translate(r); + } + if (const okl_long r = okl::sys(okl::nr_listen, fd, 128); okl::failed(r)) { + okl::sys(okl::nr_close, fd); + return okl::translate(r); + } + out->h = okl::pack(static_cast(fd)); + if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + +int kal_net_connect_at(kal_dir base, const char* name, kal_uintptr len, kal_net_conn* out) { + if (out == nullptr) return kal_err_invalid; + unsigned char addr[110] = {}; + okl_long alen = 0; + if (const int rc = local_address(base, name, len, addr, alen); rc != kal_ok) return rc; + const okl_long fd = okl::sys(okl::nr_socket, 1 /* AF_UNIX */, okl::sock_stream | okl::sock_cloexec, 0); + if (okl::failed(fd)) return okl::translate(fd); + for (;;) { + const okl_long r = okl::sys(okl::nr_connect, fd, reinterpret_cast(addr), alen); + if (okl::interrupted(r)) continue; + if (okl::failed(r)) { + okl::sys(okl::nr_close, fd); + // Nothing listening: no name, or a name a listener left behind. + return (r == -okl::e_noent || r == -111 /* ECONNREFUSED */) ? kal_err_not_found + : okl::translate(r); + } + break; + } + out->h = okl::pack(static_cast(fd)); + if (out->h == 0) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} + kal_stream kal_net_stream(kal_net_conn c) { // The bare descriptor, for the reason kal_fs_stream gives: openkal.stream's // operations take whatever the environment's transfer calls take, and a diff --git a/src/process.cpp b/src/process.cpp index c3903d2..623f58f 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -3,8 +3,15 @@ #include #include +namespace okl { extern char** g_envp; extern bool g_pipe_started_ignored; okl_ulong auxval(okl_ulong key); } + namespace { +bool same_prefix(const char* a, const char* b, okl_uptr n) { + for (okl_uptr i = 0; i < n; ++i) if (a[i] != b[i]) return false; + return true; +} + constexpr okl_uptr kMaxEntries = 512; @@ -309,6 +316,120 @@ extern "C" { // grants, and two flags. They compose, which the three declarations could not // do: there was no way to grant directories AND bind a lifetime, and no way at // all to say the two things a shell runner needs together. +// --- the handles of this interface (0.17) ------------------------------------ +// +// A started program's handle is its identifier, as it always was. Two more +// kinds exist since 0.17 and are told apart by the top byte, which no +// identifier reaches: this program itself, and a program found by its +// identifier, which carries the descriptor the kernel gave for it (`pidfd') +// in the bits above the identifier. Since 0.18 a third: a program found by +// its identifier that the kernel will not give a descriptor for (another +// namespace's, or a kernel before 5.3), observed through its /proc entry, with +// the low bits of its start in the bits above the identifier --- the start the +// open recorded, so that the identifier given to another program reads as the +// first one ended (openkal `kal_process_open_id'). +namespace { +constexpr unsigned kind_child = 0, kind_self = 1, kind_opened = 2, kind_observed = 3; +inline unsigned handle_kind(kal_process h) { return static_cast(h.h >> 56); } +inline okl_long handle_pid(kal_process h) { return static_cast(h.h & 0xffffffffu); } +} // namespace + +// --- where `execveat' cannot be used (0.18) ---------------------------------- +// +// termux's PRoot implements `execveat' for AT_FDCWD only and answers ENOSYS to a +// descriptor; in its ptrace mode it lets the kernel run the call untranslated, +// and the started program dies at its first allocation. Every start in that +// environment failed. The answer is `execve' with an absolute name, joined HERE, +// before the duplication, from the name the kernel gives the base directory --- +// the duplicate still allocates nothing. It costs the property `execveat' +// bought, that the name is resolved through the descriptor, and is paid only +// where `execveat' is unusable: from the first start under a translator this +// implementation recognises, and after the first ENOSYS anywhere else. +// +// 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. +namespace { + +int g_by_name = -1; // -1 not yet decided, 0 execveat, 1 execve + +// Whether this program is traced by PRoot: the tracer's name, and where /proc +// hides it, a PROOT_ variable. A debugger also traces and is not a translator. +bool under_translator() { + char st[4096]; + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, + reinterpret_cast("/proc/self/status"), + okl::o_rdonly | okl::o_cloexec, 0); + okl_long n = 0; + if (!okl::failed(fd)) { + n = okl::sys(okl::nr_read, fd, reinterpret_cast(st), sizeof st - 1); + okl::sys(okl::nr_close, fd); + } + if (n > 0) { + st[n] = '\0'; + const char* t = nullptr; + for (okl_long i = 0; i + 10 < n; ++i) + if (st[i] == 'T' && same_prefix(st + i, "TracerPid:", 10)) { t = st + i + 10; break; } + okl_long pid = 0; + while (t && (*t == ' ' || *t == '\t')) ++t; + while (t && *t >= '0' && *t <= '9') pid = pid * 10 + (*t++ - '0'); + if (pid == 0) return false; + char path[48] = "/proc/"; + char* o = put_decimal(path + 6, static_cast(pid)); + okl::copy(o, "/comm", 6); + const okl_long cfd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(path), + okl::o_rdonly | okl::o_cloexec, 0); + if (!okl::failed(cfd)) { + char comm[32] = {}; + const okl_long m = okl::sys(okl::nr_read, cfd, reinterpret_cast(comm), sizeof comm - 1); + okl::sys(okl::nr_close, cfd); + return m >= 5 && same_prefix(comm, "proot", 5); + } + } + for (char** e = okl::g_envp; e && *e; ++e) + if (same_prefix(*e, "PROOT_", 6)) return true; + return false; +} + +bool by_name() { + int v = __atomic_load_n(&g_by_name, __ATOMIC_ACQUIRE); + if (v < 0) { + v = under_translator() ? 1 : 0; + __atomic_store_n(&g_by_name, v, __ATOMIC_RELEASE); + } + return v == 1; +} + +// `/proc/self/fd/' read, and `name' joined beneath it. +bool absolute_under(int fd, const char* name, okl_uptr len, char* out, okl_uptr cap) { + char link[32] = "/proc/self/fd/"; + char* e = put_decimal(link + 14, static_cast(fd)); + *e = '\0'; + const okl_long r = okl::sys(okl::nr_readlinkat, okl::at_fdcwd, reinterpret_cast(link), + reinterpret_cast(out), static_cast(cap)); + if (okl::failed(r) || r <= 0) return false; + okl_uptr at = static_cast(r); + if (at + 1 + len + 1 > cap) return false; + if (out[at - 1] != '/') out[at++] = '/'; + okl::copy(out + at, name, len); + out[at + len] = '\0'; + return true; +} + +} // namespace + +// EVERY ENVIRONMENT ENTRY IS `NAME=value' (0.17, process.h). Refused before +// anything is built, so that one implementation does not pass on an entry +// another refuses. +static bool entries_well_formed(const char** envp, const kal_uintptr* lens, kal_uintptr n) { + for (kal_uintptr i = 0; i < n; ++i) { + if (envp[i] == nullptr) return false; + kal_uintptr eq = 0; + while (eq < lens[i] && envp[i][eq] != '=') ++eq; + if (eq == 0 || eq == lens[i]) return false; + } + return true; +} + 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, @@ -348,6 +469,15 @@ int kal_process_spawn(const kal_spawn* how, granted[i] = fd; } + if (envc > 0 && (envp == nullptr || envp_lens == nullptr)) return kal_err_invalid; + if (!entries_well_formed(envp, envp_lens, envc)) return kal_err_invalid; + + // The absolute name, for `execve' where `execveat' cannot be used. Computed + // always, because the first start that meets ENOSYS learns it in the child. + char resolved[2 * okl::max_name + 2]; + const bool named = absolute_under(b, p.buf, path_len, resolved, sizeof resolved); + const bool use_name = by_name() && named; + 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)) @@ -445,6 +575,15 @@ int kal_process_spawn(const kal_spawn* how, // 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 @@ -517,6 +656,14 @@ int kal_process_spawn(const kal_spawn* how, // // 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)) { @@ -536,6 +683,10 @@ int kal_process_spawn(const kal_spawn* how, 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); @@ -604,6 +755,8 @@ void kal_process_channel_close(kal_stream s) { int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) { if (h.h == 0) return kal_err_invalid; + if (handle_kind(h) == kind_self) return kal_err_invalid; + if (handle_kind(h) == kind_opened || handle_kind(h) == kind_observed) return kal_err_not_supported; int st = 0; for (;;) { const okl_long r = okl::sys(okl::nr_wait4, static_cast(h.h), @@ -635,6 +788,8 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) // the caller says which of the two it means. int kal_process_terminate(kal_process h) { if (h.h == 0) return kal_err_invalid; + if (handle_kind(h) == kind_self) return kal_err_invalid; + if (handle_kind(h) == kind_opened || handle_kind(h) == kind_observed) return kal_err_not_supported; const okl_long r = okl::sys(okl::nr_kill, static_cast(h.h), 15 /* SIGTERM */); return okl::failed(r) ? okl::translate(r) : kal_ok; } @@ -697,7 +852,12 @@ void kal_process_job_close(kal_job) { } // Releasing the handle does not affect the program. A program that has not been // waited for continues, and this environment collects it when the caller exits. -void kal_process_close(kal_process) { } +void kal_process_close(kal_process h) { + if (handle_kind(h) == kind_opened) { + const okl_long fd = static_cast((h.h >> 32) & 0xffffu) - 1; + if (fd >= 0) okl::sys(okl::nr_close, fd); + } +} // A WORD THE ENVIRONMENT SETS WHEN SOMEBODY HAS ASKED THIS PROGRAM TO END. // @@ -726,14 +886,27 @@ int g_stop_armed = 0; #if defined(__x86_64__) extern "C" void okl_sigreturn_trampoline(void); -asm(".globl okl_sigreturn_trampoline\n" +// IN `.text', SAID IN SO MANY WORDS (0.18). A top-level `asm' is emitted into +// whatever section the compiler is in at that point of the file, and GCC at +// -O0 was in `.bss': the three instructions were zero bytes in memory that may +// not be executed, and the first request to end a program faulted on its way +// back from the handler (measured: SIGSEGV after SIGTERM, GCC -O0 only). +asm(".pushsection .text,\"ax\",@progbits\n" + ".globl okl_sigreturn_trampoline\n" "okl_sigreturn_trampoline:\n" " movq $15, %rax\n" // rt_sigreturn - " syscall\n"); + " syscall\n" + ".popsection\n"); constexpr unsigned long sa_restorer_flag = 0x04000000u; // SA_RESTORER #endif -void stop_handler(int) { +// Which request arrived first, for `kal_process_end_as_requested'. +int g_stop_signal = 0; + +void stop_handler(int signo) { + int none = 0; + __atomic_compare_exchange_n(&g_stop_signal, &none, signo, false, + __ATOMIC_RELAXED, __ATOMIC_RELAXED); __atomic_store_n(&g_stop_word, 1u, __ATOMIC_RELEASE); okl::sys(okl::nr_futex, reinterpret_cast(&g_stop_word), 1 /* FUTEX_WAKE */, 0x7fffffff, 0, 0, 0); @@ -748,6 +921,16 @@ void stop_handler(int) { // mode obvious. bool arm_one(int signo) { struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } act {}; + // A REQUEST THE STARTER ARRANGED NOT TO DELIVER STAYS UNDELIVERED (0.17, + // the take-over rule). A program started in the background is started with + // these requests ignored, and taking them over would end it when its + // starter meant it to survive them --- which this did until 0.18. + { + decltype(act) old{}; + const okl_long r = okl::sys(okl::nr_rt_sigaction, signo, 0, + reinterpret_cast(&old), sizeof old.mask); + if (!okl::failed(r) && old.handler == reinterpret_cast(1) /* SIG_IGN */) return true; + } act.handler = reinterpret_cast(&stop_handler); #if defined(__x86_64__) act.flags = sa_restorer_flag; @@ -775,12 +958,261 @@ const kal_u32* kal_process_stop_requested(void) { return state == 1 ? &g_stop_word : nullptr; } +// --- what is known of a program (0.17) -------------------------------------- +namespace { + +// `pidfd_open' is the kernel's handle on a program, and it is what lets a +// handle found by an identifier keep naming that program after the number is +// given again (5.3 and later). Asked once. +int g_pidfd = -1; +bool open_id_available() { + int v = __atomic_load_n(&g_pidfd, __ATOMIC_ACQUIRE); + if (v < 0) { + const okl_long fd = okl::sys(okl::nr_pidfd_open, okl::sys(okl::nr_getpid), 0); + v = okl::failed(fd) ? 0 : 1; + if (!okl::failed(fd)) okl::sys(okl::nr_close, fd); + __atomic_store_n(&g_pidfd, v, __ATOMIC_RELEASE); + } + return v == 1; +} + +// /proc//stat, or /proc/self/stat for pid 0, read whole into `b'; the +// length, or 0 when there is no such entry. +okl_long read_stat(okl_long pid, char (&b)[1024]) { + char path[40] = "/proc/"; + char* o = path + 6; + if (pid == 0) { okl::copy(o, "self", 4); o += 4; } + else o = put_decimal(o, static_cast(pid)); + okl::copy(o, "/stat", 6); + const okl_long fd = okl::sys(okl::nr_openat, okl::at_fdcwd, reinterpret_cast(path), + okl::o_rdonly | okl::o_cloexec, 0); + if (okl::failed(fd)) return 0; + const okl_long n = okl::sys(okl::nr_read, fd, reinterpret_cast(b), sizeof b - 1); + okl::sys(okl::nr_close, fd); + return n > 0 ? n : 0; +} + +// The fields after the name, which is in parentheses and may itself contain +// them, so the last `)' is where the fields begin: the position of field +// `wanted' (the state is 3, the processor times 14 and 15, the start 22), or -1. +okl_long stat_field(const char* b, okl_long n, int wanted) { + okl_long close = -1; + for (okl_long i = 0; i < n; ++i) if (b[i] == ')') close = i; + if (close < 0) return -1; + int field = 2; // the name was field 2 + okl_long i = close + 1; + while (i < n && field < wanted) { if (b[i] == ' ') ++field; ++i; } + return field == wanted && i < n ? i : -1; +} + +okl_u64 stat_number(const char* b, okl_long n, int wanted, bool& ok) { + const okl_long at = stat_field(b, n, wanted); + ok = at >= 0; + okl_u64 v = 0; + for (okl_long i = at; ok && i < n && b[i] >= '0' && b[i] <= '9'; ++i) v = v * 10 + static_cast(b[i] - '0'); + return v; +} + +// Field 22: when the program started, in the kernel's clock ticks since boot. +// Of this program (pid 0) through /proc/self: under a sandbox that numbers its +// programs apart from the /proc it shows, `getpid' names another program's entry +// --- measured beneath a language server's own sandbox, every instance read pid +// 1's start, and a killed instance and its restart were one program. +bool started_ticks(okl_long pid, okl_u64& ticks) { + char b[1024]; + const okl_long n = read_stat(pid, b); + bool ok = false; + ticks = stat_number(b, n, 22, ok); + return ok; +} + +// The state letter (field 3), or 0 when there is no entry: a program whose +// entry is gone, or is Z or X, has ended. +char stat_state(okl_long pid) { + char b[1024]; + const okl_long n = read_stat(pid, b); + const okl_long at = stat_field(b, n, 3); + return at >= 0 ? b[at] : 0; +} + +// This program's number as the /proc it shows names it, which is the number +// another program finds it by there; `getpid' where there is no /proc. +okl_long self_number() { + char b[24]; + const okl_long n = okl::sys(okl::nr_readlinkat, okl::at_fdcwd, reinterpret_cast("/proc/self"), + reinterpret_cast(b), sizeof b); + okl_long v = 0; + if (n > 0) for (okl_long i = 0; i < n && b[i] >= '0' && b[i] <= '9'; ++i) v = v * 10 + (b[i] - '0'); + return v > 0 ? v : okl::sys(okl::nr_getpid); +} + +okl_u64 ticks_to_ns(okl_u64 t) { + okl_ulong hz = okl::auxval(17 /* AT_CLKTCK */); + if (hz == 0) hz = 100; + return t / hz * 1000000000ull + t % hz * (1000000000ull / hz); +} + +// The processor time of a program, through the clock the kernel keeps for +// every program (`MAKE_PROCESS_CPUCLOCK(pid, CPUCLOCK_SCHED)'): nanoseconds, +// for any program the caller may ask about. +// +// A program this one cannot name to the kernel --- one numbered by another +// namespace, observed through /proc --- is answered from its entry's processor +// times (fields 14 and 15, in clock ticks). +bool cpu_ns(okl_long pid, bool self, okl_u64& out) { + const okl_long clock = self ? 2 /* CLOCK_PROCESS_CPUTIME_ID */ + : static_cast(static_cast((~static_cast(pid)) << 3) | 2); + struct { okl_long sec, nsec; } ts{}; + const okl_long r = okl::sys(okl::nr_clock_gettime, clock, reinterpret_cast(&ts)); + if (!okl::failed(r)) { + out = static_cast(ts.sec) * 1000000000ull + static_cast(ts.nsec); + return true; + } + char b[1024]; + const okl_long n = read_stat(self ? 0 : pid, b); + bool user_ok = false, system_ok = false; + const okl_u64 user = stat_number(b, n, 14, user_ok), system = stat_number(b, n, 15, system_ok); + if (!user_ok || !system_ok) return false; + out = ticks_to_ns(user + system); + return true; +} + +// Whether a program has ended. A started one: `waitid' without collecting it +// (WNOWAIT), and one already collected has ended too. One found by its +// identifier: its descriptor is readable once it has ended. +bool has_ended(kal_process h) { + if (handle_kind(h) == kind_self) return false; + if (handle_kind(h) == kind_observed) { + const char state = stat_state(handle_pid(h)); + okl_u64 t = 0; + return state == 0 || state == 'Z' || state == 'X' || !started_ticks(handle_pid(h), t) + || (t & 0xffffffu) != ((h.h >> 32) & 0xffffffu); + } + if (handle_kind(h) == kind_opened) { + const okl_long fd = static_cast((h.h >> 32) & 0xffffu) - 1; + if (fd < 0) return false; + struct { int fd; short events, revents; } pfd{ static_cast(fd), 1 /* POLLIN */, 0 }; + struct { okl_long sec, nsec; } zero{0, 0}; + const okl_long r = okl::sys(okl::nr_ppoll, reinterpret_cast(&pfd), 1, + reinterpret_cast(&zero), 0, 0); + return r > 0 && (pfd.revents & 1) != 0; + } + alignas(8) unsigned char info[128] = {}; + const okl_long r = okl::sys(okl::nr_waitid, 1 /* P_PID */, handle_pid(h), + reinterpret_cast(info), + 4 | 1 | 0x01000000 /* WEXITED | WNOHANG | WNOWAIT */, 0); + if (r == -okl::e_child) return true; + if (okl::failed(r)) return false; + int si_pid = 0; + okl::copy(reinterpret_cast(&si_pid), reinterpret_cast(info + 16), sizeof si_pid); + return si_pid != 0; +} + +} // namespace + +int kal_process_self(kal_process* out) { + if (out == nullptr) return kal_err_invalid; + *out = kal_process{ (static_cast(kind_self) << 56) + | static_cast(okl::sys(okl::nr_getpid)) }; + return kal_ok; +} + +int kal_process_open_id(kal_u64 id, kal_process* out) { + if (out == nullptr || id == 0 || id > 0x7fffffffu) return id == 0 || out == nullptr + ? kal_err_invalid : kal_err_not_found; + // NUMBERED AS /proc NUMBERS IT, WHERE THE KERNEL DOES NOT. A program in + // another namespace than this one's --- a sandbox that shows the system's + // /proc --- is not found by `pidfd_open', and a kernel before 5.3 has no + // such call; its entry is observed instead, with no descriptor: ended when + // the entry is gone or is a zombie's. + const auto observed = [&]() -> int { + const char state = stat_state(static_cast(id)); + okl_u64 t = 0; + if (state == 0 || state == 'Z' || state == 'X' || !started_ticks(static_cast(id), t)) + return kal_err_not_found; + *out = kal_process{ (static_cast(kind_observed) << 56) + | (static_cast(t & 0xffffffu) << 32) | static_cast(id) }; + return kal_ok; + }; + if (!open_id_available()) { + if (stat_state(0) == 0) return kal_err_not_supported; + return observed(); + } + const okl_long fd = okl::sys(okl::nr_pidfd_open, static_cast(id), 0); + if (fd == -3 /* ESRCH */) return observed(); + if (okl::failed(fd)) return okl::translate(fd); + if (fd >= 0xffff) { okl::sys(okl::nr_close, fd); return kal_err_no_memory; } + okl::sys(okl::nr_fcntl, fd, okl::f_setfd, okl::fd_cloexec); + *out = kal_process{ (static_cast(kind_opened) << 56) + | (static_cast(fd + 1) << 32) + | static_cast(id) }; + return kal_ok; +} + +int kal_process_info(kal_process p, kal_u32 wanted, kal_program_info* out) { + if (out == nullptr || p.h == 0 || out->self_size < 8) return kal_err_invalid; + const kal_u32 self = out->self_size; + kal_program_info v{}; + v.self_size = self; + const bool me = handle_kind(p) == kind_self; + const okl_long pid = handle_pid(p); + v.id = static_cast(me ? self_number() : pid); + v.present |= KAL_PINFO_ID; + const bool ended = has_ended(p); + v.ended = ended ? 1u : 0u; + v.present |= KAL_PINFO_ENDED; + if (!ended && (wanted & KAL_PINFO_STARTED)) { + okl_u64 t = 0; + // Read, then checked again: a program found by its identifier that + // ended meanwhile may have had its number given to another, whose start + // this would have read. + if (started_ticks(me ? 0 : pid, t) && !has_ended(p)) { + v.started_ns = ticks_to_ns(t); + v.present |= KAL_PINFO_STARTED; + } + } + if (!ended && (wanted & KAL_PINFO_CPU)) { + okl_u64 c = 0; + if (cpu_ns(pid, me, c)) { v.cpu_ns = c; v.present |= KAL_PINFO_CPU; } + } + const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); + okl::copy(reinterpret_cast(out), reinterpret_cast(&v), n); + return kal_ok; +} + +// ENDS AS THE REQUEST WOULD HAVE ENDED THE PROGRAM (0.17): the request's own +// default treatment restored, the request unblocked and sent again. Its +// default is to end the program, so the starter's wait reports the signal; a +// program that is somehow still here a moment later ends with the status a +// shell gives such a program. +int kal_process_end_as_requested(void) { + if (__atomic_load_n(&g_stop_word, __ATOMIC_ACQUIRE) == 0) return kal_err_invalid; + int signo = __atomic_load_n(&g_stop_signal, __ATOMIC_RELAXED); + if (signo == 0) signo = 15; + struct { void* handler; unsigned long flags; void* restorer; unsigned long mask; } act {}; +#if defined(__x86_64__) + act.flags = sa_restorer_flag; + act.restorer = reinterpret_cast(&okl_sigreturn_trampoline); +#endif + okl::sys(okl::nr_rt_sigaction, signo, reinterpret_cast(&act), 0, sizeof act.mask); + unsigned long set = 1ul << (signo - 1); + okl::sys(okl::nr_rt_sigprocmask, 1 /* SIG_UNBLOCK */, reinterpret_cast(&set), 0, sizeof set); + okl::sys(okl::nr_kill, okl::sys(okl::nr_getpid), signo); + for (int i = 0; i < 100; ++i) { + struct { okl_long sec, nsec; } ts{0, 1000000}; + okl::sys(okl::nr_nanosleep, reinterpret_cast(&ts), 0); + } + okl::sys(okl::nr_exit_group, 128 + signo); + for (;;) { } +} + 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_BOUND_LIFETIME | KAL_PROCESS_PROP_JOB + | (open_id_available() || stat_state(0) != 0 ? KAL_PROCESS_PROP_OPEN_ID : 0u) // AGREES WITH `kal_process_stop_requested', because the header // defines null there as the absence this position reports. Read and // never armed: asking what an implementation can do must not install a diff --git a/src/sys.h b/src/sys.h index 53006e0..c995553 100644 --- a/src/sys.h +++ b/src/sys.h @@ -43,6 +43,14 @@ namespace okl { #if defined(__x86_64__) +// EVERY ARGUMENT REGISTER IS AN IN/OUT OPERAND (0.18). The kernel preserves +// them, and the operands used to say so; a system-call translator does not. +// termux's PRoot, in its default seccomp mode, rewrites the register that +// carries the path of `openat' and leaves it rewritten, so a value the compiler +// kept "in rsi" across the call was the translator's and not the program's +// --- measured: the `"/"' preopen lost its name and every program path was +// "outside every preopened directory". Declaring the registers written costs +// nothing the kernel's own convention did not already allow. inline okl_long sys(okl_long n) { okl_ulong r; __asm__ __volatile__("syscall" : "=a"(r) : "a"(n) : "rcx", "r11", "memory"); @@ -50,38 +58,38 @@ inline okl_long sys(okl_long n) { } inline okl_long sys(okl_long n, okl_long a) { okl_ulong r; - __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a) : "rcx", "r11", "memory"); + __asm__ __volatile__("syscall" : "=a"(r), "+D"(a) : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } inline okl_long sys(okl_long n, okl_long a, okl_long b) { okl_ulong r; - __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b) : "rcx", "r11", "memory"); + __asm__ __volatile__("syscall" : "=a"(r), "+D"(a), "+S"(b) : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c) { okl_ulong r; - __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c) + __asm__ __volatile__("syscall" : "=a"(r), "+D"(a), "+S"(b), "+d"(c) : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d) { okl_ulong r; register okl_long r10 __asm__("r10") = d; - __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10) + __asm__ __volatile__("syscall" : "=a"(r), "+D"(a), "+S"(b), "+d"(c), "+r"(r10) : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, okl_long e) { okl_ulong r; register okl_long r10 __asm__("r10") = d; register okl_long r8 __asm__("r8") = e; - __asm__ __volatile__("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8) - : "rcx", "r11", "memory"); + __asm__ __volatile__("syscall" : "=a"(r), "+D"(a), "+S"(b), "+d"(c), "+r"(r10), "+r"(r8) + : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, okl_long e, okl_long f) { okl_ulong r; register okl_long r10 __asm__("r10") = d; register okl_long r8 __asm__("r8") = e; register okl_long r9 __asm__("r9") = f; - __asm__ __volatile__("syscall" : "=a"(r) - : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9) - : "rcx", "r11", "memory"); + __asm__ __volatile__("syscall" + : "=a"(r), "+D"(a), "+S"(b), "+d"(c), "+r"(r10), "+r"(r8), "+r"(r9) + : "a"(n) : "rcx", "r11", "memory"); return static_cast(r); } @@ -117,6 +125,10 @@ enum : okl_long { // and poll takes milliseconds, so poll could not express a bound finer than // the granularity this implementation reports. nr_ppoll = 271, + // openkal 0.17: a rename that does not replace, a program found by its + // number, how one ended, and the request reproduced. + nr_renameat2 = 316, nr_pidfd_open = 434, nr_waitid = 247, + nr_rt_sigprocmask = 14, }; #elif defined(__aarch64__) @@ -141,20 +153,20 @@ inline okl_long sys(okl_long n, okl_long a) { inline okl_long sys(okl_long n, okl_long a, okl_long b) { register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; register okl_long x1 __asm__("x1") = b; - __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1) : "memory", "cc"); + __asm__ __volatile__("svc 0" : "+r"(x0), "+r"(x1) : "r"(x8) : "memory", "cc"); return x0; } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c) { register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; - __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2) : "memory", "cc"); + __asm__ __volatile__("svc 0" : "+r"(x0), "+r"(x1), "+r"(x2) : "r"(x8) : "memory", "cc"); return x0; } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d) { register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; register okl_long x3 __asm__("x3") = d; - __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3) + __asm__ __volatile__("svc 0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) : "r"(x8) : "memory", "cc"); return x0; } @@ -162,8 +174,8 @@ inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, register okl_long x8 __asm__("x8") = n; register okl_long x0 __asm__("x0") = a; register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; register okl_long x3 __asm__("x3") = d; register okl_long x4 __asm__("x4") = e; - __asm__ __volatile__("svc 0" : "+r"(x0) : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4) - : "memory", "cc"); + __asm__ __volatile__("svc 0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3), "+r"(x4) + : "r"(x8) : "memory", "cc"); return x0; } inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, @@ -172,9 +184,8 @@ inline okl_long sys(okl_long n, okl_long a, okl_long b, okl_long c, okl_long d, register okl_long x1 __asm__("x1") = b; register okl_long x2 __asm__("x2") = c; register okl_long x3 __asm__("x3") = d; register okl_long x4 __asm__("x4") = e; register okl_long x5 __asm__("x5") = f; - __asm__ __volatile__("svc 0" : "+r"(x0) - : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5) - : "memory", "cc"); + __asm__ __volatile__("svc 0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3), "+r"(x4), "+r"(x5) + : "r"(x8) : "memory", "cc"); return x0; } @@ -211,6 +222,9 @@ enum : okl_long { // openkal.timeout. This architecture has no `poll' at all, only `ppoll', // which is a second reason the bound is expressed through the latter. nr_ppoll = 73, + // openkal 0.17, as on the other architecture. + nr_renameat2 = 276, nr_pidfd_open = 434, nr_waitid = 95, + nr_rt_sigprocmask = 135, }; #else diff --git a/src/terminal.cpp b/src/terminal.cpp index ddba069..5da5785 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -37,6 +37,10 @@ kal_uintptr mode_of(const okl::ktermios& t) { if ((t.lflag & (okl::t_isig | okl::t_iexten)) == 0 && (t.iflag & okl::t_ixon) == 0) m |= KAL_TERM_PASS_CONTROL; if ((t.iflag & kAlters) == 0) m |= KAL_TERM_PASS_INPUT; + // A terminal of this kernel is an emulator's or a console's, and both obey + // ECMA-48 whatever is asked (0.17): the position is reported set, and a + // request to clear it is ignored, as the declaration says. + m |= KAL_TERM_SEQUENCES; return m; } diff --git a/tests/identity_in_a_pid_namespace.cpp b/tests/identity_in_a_pid_namespace.cpp new file mode 100644 index 0000000..364b7e5 --- /dev/null +++ b/tests/identity_in_a_pid_namespace.cpp @@ -0,0 +1,93 @@ +// A program's identity where the sandbox numbers its programs apart from the +// /proc it shows (0.18). +// +// A pid namespace whose /proc is the system's: `getpid' answers 1 inside, and +// /proc/1 is another program. Measured beneath a language server's sandbox, +// every instance named itself pid 1 with pid 1's start, so a killed instance and +// its restart were one program. Here a program in such a namespace names itself +// by the number /proc gives it, and a program outside finds it by that number +// with the same start; and the program inside finds the one outside, which +// `pidfd_open' cannot reach from there, through its /proc entry. +#include +#include +#include +#include +import openkal.types; +import openkal.process; + +namespace { +struct identity { kal_u64 id = 0, started = 0; int ended = 0; }; + +bool describe(kal_process p, identity& out) { + kal_program_info info{}; + info.self_size = sizeof info; + if (kal_process_info(p, 0xffffffffu, &info) != kal_ok) return false; + out = { info.id, info.started_ns, static_cast(info.ended) }; + return true; +} +bool self(identity& out) { + kal_process me{}; + if (kal_process_self(&me) != kal_ok) return false; + const bool ok = describe(me, out); + kal_process_close(me); + return ok; +} +bool find(kal_u64 id, identity& out) { + kal_process p{}; + if (kal_process_open_id(id, &p) != kal_ok) return false; + const bool ok = describe(p, out); + kal_process_close(p); + return ok; +} +} + +int main() { + identity outside{}; + if (!self(outside)) { std::printf("FAIL: this program names itself\n"); return 1; } + int report[2], release[2]; + if (::pipe(report) != 0 || ::pipe(release) != 0) return 1; + const pid_t helper = ::fork(); + if (helper == 0) { + if (::unshare(CLONE_NEWUSER | CLONE_NEWPID) != 0) { + identity none{}; + (void)!::write(report[1], &none, sizeof none); + ::_exit(0); + } + const pid_t inner = ::fork(); + if (inner == 0) { + identity me{}, parent{}; + const bool named = self(me); + const bool found = find(outside.id, parent); + const int failures = (!named || me.id == 1) + (!found || parent.ended || parent.started != outside.started); + if (!named || me.id == 1) std::printf("FAIL: inside the namespace this program is named %llu\n", (unsigned long long)me.id); + if (!found || parent.ended || parent.started != outside.started) + std::printf("FAIL: inside the namespace the program outside is %s\n", found ? "another" : "not found"); + (void)!::write(report[1], &me, sizeof me); + char go; + (void)!::read(release[0], &go, 1); + ::_exit(failures); + } + int st = 0; + ::waitpid(inner, &st, 0); + ::_exit(WIFEXITED(st) ? WEXITSTATUS(st) : 1); + } + identity inner{}; + (void)!::read(report[0], &inner, sizeof inner); + int failures = 0; + if (inner.id == 0) { + std::printf("openkal-linux: no pid namespace can be made here; nothing to observe\n"); + } else { + identity seen{}; + if (!find(inner.id, seen) || seen.ended || seen.started != inner.started) { + std::printf("FAIL: outside, the program inside is not found by the number it named itself by\n"); + ++failures; + } + } + (void)!::write(release[1], "x", 1); + int st = 0; + ::waitpid(helper, &st, 0); + if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) ++failures; + if (failures == 0 && inner.id != 0) + std::printf("openkal-linux: a program in a pid namespace is named as /proc names it, both ways\n"); + return failures; +} diff --git a/tests/sigpipe_not_passed_on.cpp b/tests/sigpipe_not_passed_on.cpp new file mode 100644 index 0000000..f8bed46 --- /dev/null +++ b/tests/sigpipe_not_passed_on.cpp @@ -0,0 +1,93 @@ +// The ignore of SIGPIPE is this program's own (src/env.cpp) and is not passed +// on (0.18): a program started through `kal_process_spawn' begins with SIGPIPE +// as this program was started with --- the default for an ordinary start, so a +// started shell's `yes | head' ends by the signal; and still ignored for a +// program that was itself started with it ignored. +// +// The disposition a program was started with is observed before openkal's own +// initialiser runs: an initialiser of priority 100 runs before one of 101. Both +// starts are made here, with the disposition set explicitly: what this test +// itself was started with is the harness's (a CI runner starts its steps with +// SIGPIPE ignored). +#include +#include +#include +#include +#include +import openkal.types; +import openkal.env; +import openkal.fs; +import openkal.process; + +namespace { +struct sigaction g_started{}; +[[gnu::constructor(100)]] void observe() { ::sigaction(SIGPIPE, nullptr, &g_started); } + +int failures = 0; +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} + +kal_dir root() { + for (kal_uintptr i = 0; i < kal_fs_preopen_count(); ++i) { + kal_dir d{}; char n[8]; kal_uintptr l = 0; + if (kal_fs_preopen(i, &d, n, sizeof n, &l) == kal_ok && l == 1 && n[0] == '/') return d; + } + return kal_dir{}; +} + +char g_self[4096]; + +// This program again, through openkal, asked how it was started: 7 ignored, +// 0 default. +int started_child_reports() { + const kal_dir r = root(); + const char* argv[] = { g_self, "--report" }; + const kal_uintptr lens[] = { std::strlen(g_self), 8 }; + const kal_spawn how{ r, r, nullptr, nullptr, 0, 0 }; + kal_process p{}; + const int e = kal_process_spawn(&how, g_self + 1, std::strlen(g_self + 1), argv, lens, 2, + nullptr, nullptr, 0, nullptr, &p); + int status = -1, terminated = 0; + if (e == kal_ok) kal_process_wait(p, &status, &terminated); + return e == kal_ok && terminated == 0 ? status : -1; +} +} // namespace + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "--report") == 0) + return g_started.sa_handler == SIG_IGN ? 7 : 0; + const kal_intptr n = kal_env_image(g_self, sizeof g_self - 1); + if (n <= 0 || g_self[0] != '/') { std::printf("FAIL: the program's own image is named\n"); return 1; } + g_self[n] = 0; + if (argc > 1 && std::strcmp(argv[1], "--started-default") == 0) { + struct sigaction now{}; + ::sigaction(SIGPIPE, nullptr, &now); + if (now.sa_handler != SIG_IGN) return 2; // this program's own writes are not ended by it + return started_child_reports() == 0 ? 0 : 1; + } + if (argc > 1 && std::strcmp(argv[1], "--started-ignoring") == 0) + return started_child_reports() == 7 ? 0 : 1; + + // This program again, started with SIGPIPE as `disposition' says: 0 when + // the program it then starts through openkal reported what was expected. + auto started_with = [](void (*disposition)(int), const char* mode) { + const pid_t child = ::fork(); + if (child == 0) { + ::signal(SIGPIPE, disposition); + ::execl(g_self, g_self, mode, nullptr); + ::_exit(93); + } + int st = 0; + ::waitpid(child, &st, 0); + return WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st); + }; + const int d = started_with(SIG_DFL, "--started-default"); + check(d != 2, "this program's own writes are not ended by SIGPIPE"); + check(d == 0, "a program started with SIGPIPE at its default starts others with it at its default"); + check(started_with(SIG_IGN, "--started-ignoring") == 0, + "a program started with SIGPIPE ignored passes the ignore on, as it was given"); + + if (failures == 0) std::printf("openkal-linux: SIGPIPE ignored here, not passed on\n"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/spawn_without_execveat.cpp b/tests/spawn_without_execveat.cpp new file mode 100644 index 0000000..4601ed7 --- /dev/null +++ b/tests/spawn_without_execveat.cpp @@ -0,0 +1,59 @@ +// A start where `execveat' answers ENOSYS (0.18, K11). +// +// termux's PRoot implements `execveat' only for AT_FDCWD. A seccomp filter +// that answers ENOSYS to `execveat' reproduces it without PRoot: every start +// failed as `kal_err_not_supported' before 0.18, and now the implementation +// starts the program with `execve' and an absolute name. +#include +#include +#include +#include +#include +#include +#include +import openkal.types; +import openkal.fs; +import openkal.process; + +namespace { +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 refuse_execveat() { + sock_filter code[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_execveat, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | 38 /* ENOSYS */), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + sock_fprog prog{ static_cast(sizeof code / sizeof code[0]), code }; + if (::prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) return false; + return ::syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog) == 0; +} +} + +int main() { + if (!refuse_execveat()) { + std::printf("openkal-linux: seccomp is not available here; nothing to observe\n"); + return 0; + } + const kal_dir r = root(); + const char* argv[] = { "sh", "-c", "exit 7" }; + const kal_uintptr lens[] = { 2, 2, 6 }; + const kal_spawn how{ r, r, nullptr, nullptr, 0, 0 }; + kal_process p{}; + const int e = kal_process_spawn(&how, "bin/sh", 6, argv, lens, 3, nullptr, nullptr, 0, nullptr, &p); + int status = -1, terminated = 0; + if (e == kal_ok) kal_process_wait(p, &status, &terminated); + if (e != kal_ok || terminated != 0 || status != 7) { + std::printf("FAIL: a program is started where execveat answers ENOSYS (rc %d, status %d)\n", e, status); + return 1; + } + std::printf("openkal-linux: a program is started where execveat answers ENOSYS\n"); + return 0; +} diff --git a/tests/stop_word_take_over.cpp b/tests/stop_word_take_over.cpp new file mode 100644 index 0000000..1134b70 --- /dev/null +++ b/tests/stop_word_take_over.cpp @@ -0,0 +1,63 @@ +// The stop word, taken over and given back (openkal 0.17, process.h; K1, K2). +// +// Three facts, each of which this implementation got wrong before 0.18: +// 1. a request delivered after the word is armed returns from the handler --- +// under GCC -O0 the return trampoline was in `.bss' and the program died +// with SIGSEGV on its way back (K1); +// 2. a request the starter arranged not to deliver stays undelivered: SIGINT +// ignored before arming is still ignored after it (K2); +// 3. ending as requested ends the program as the request would have, which a +// started copy reports to its starter as a signal and not as a status (G4). +#include +#include +#include +#include +#include +import openkal.types; +import openkal.process; +import openkal.task; + +namespace { +int failures = 0; +void check(bool held, const char* what) { + if (!held) { std::printf("FAIL: %s\n", what); ++failures; } +} +} + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "--end-as-requested") == 0) { + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) return 90; + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) return 91; + kal_process_end_as_requested(); + return 92; + } + + std::signal(SIGINT, SIG_IGN); + const kal_u32* word = kal_process_stop_requested(); + check(word != nullptr, "the stop word is armed"); + if (word == nullptr) return 1; + + ::kill(::getpid(), SIGINT); + ::usleep(20000); + check(*word == 0, "a request ignored before arming is still not delivered"); + + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + check(*word != 0, "a request after arming sets the word, and the handler returns"); + + const pid_t child = ::fork(); + if (child == 0) { + ::execl("/proc/self/exe", argv[0], "--end-as-requested", nullptr); + ::_exit(93); + } + int st = 0; + ::waitpid(child, &st, 0); + check(WIFSIGNALED(st) && WTERMSIG(st) == SIGTERM, + "a program that ends as requested is reported to its starter as ended by the request"); + + if (failures == 0) std::printf("openkal-linux: the stop word is taken over and given back\n"); + return failures == 0 ? 0 : 1; +}