diff --git a/README.md b/README.md index e1c350e..c3b2959 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ written on the kernel's own calls. ```toml [dependencies] -openkal = "0.16.0" +openkal = "0.17.0" [target.'cfg(os = "macos")'.dependencies] -openkal-macos = "0.14.1" +openkal-macos = "0.15.0" ``` Its purpose is as much to test the specification as to be used. A specification diff --git a/mcpp.toml b/mcpp.toml index 751f1ce..933edff 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-macos" -version = "0.14.1" +version = "0.15.0" description = "An implementation of openkal for macOS, written on the kernel's own calls. Its purpose is as much to test the specification as to be used." license = "Apache-2.0" @@ -57,7 +57,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.16.0" +openkal = "0.17.0" [build] # The flags are attached to this package's own sources rather than to the whole diff --git a/src/endpoint.h b/src/endpoint.h index 96832f4..af709cd 100644 --- a/src/endpoint.h +++ b/src/endpoint.h @@ -106,6 +106,10 @@ inline int from_kernel(const ksockaddr_storage& in, kal_endpoint& out) { return kal_ok; } + // A connection made at a name (0.17) has no address and port to state; the + // kernel reports the local family, or an unnamed one for an accepted end. + if (family == 1 /* AF_UNIX */ || family == 0) return kal_err_not_supported; + return kal_err_invalid; } diff --git a/src/env.cpp b/src/env.cpp index 243feab..b99e2cc 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -10,6 +10,9 @@ namespace okm { int g_argc = 0; char** g_argv = nullptr; char** g_envp = nullptr; +// Whether SIGPIPE was ignored HERE, at the default this program was started +// with: a program it starts is given the default back (process.cpp). +bool g_pipe_ignored_here = false; void record(int argc, char** argv, char** envp) { g_argc = argc; g_argv = argv; g_envp = envp; @@ -41,14 +44,33 @@ namespace { // mistake that shows up as a program dying in a way nobody can trace --- which is // the defect this note is about, arrived at from the other side. // -// ⇒ It is left until it can be MEASURED on this system. This repository already -// refuses to claim a facility it has not exercised, and a signal disposition -// installed by guesswork is exactly that. The consequence meanwhile is stated: -// a program above this implementation that writes to a stream whose far end has -// gone is ended by SIGPIPE rather than told, and no layer between it and here -// can name what happened. +// ⇒ MEASURED SINCE (0.15): the stop word installs a handler through this +// kernel's `sigaction' with this implementation's own trampoline, and the +// conformance suite raises the signal and returns from it. Ignoring needs no +// trampoline at all --- the kernel never enters one for an ignored signal --- +// so the shape is the measured one and the trampoline field is left empty. +// +// ONLY WHERE NOTHING ELSE HAS CHOSEN. A program whose own runtime installed a +// handler for SIGPIPE keeps it; a disposition still at its default is the one +// that ends the program without telling it, and that is the one replaced. +// +// AND IT IS THIS PROGRAM'S, NOT PASSED ON. An ignored disposition survives +// `execve', so a program started from here would begin with SIGPIPE ignored and +// a shell it runs could not undo it. `kal_process_spawn' gives the default back +// to the program it starts when the ignore was made here (process.cpp). +void ignore_sigpipe() { + struct { void (*handler)(int); void* tramp; unsigned int mask; int flags; } act{}; + struct { void (*handler)(int); unsigned int mask; int flags; } old{}; + if (okm::failed(okm::sys(46 /* sigaction */, 13 /* SIGPIPE */, 0, reinterpret_cast(&old)))) + return; + if (old.handler != nullptr) return; // not SIG_DFL + act.handler = reinterpret_cast(1); // SIG_IGN + if (!okm::failed(okm::sys(46 /* sigaction */, 13, reinterpret_cast(&act), 0))) + okm::g_pipe_ignored_here = true; +} [[gnu::constructor(101)]] void capture(int argc, char** argv, char** envp) { if (okm::g_argv == nullptr) okm::record(argc, argv, envp); + ignore_sigpipe(); } } // namespace @@ -108,4 +130,42 @@ 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): what `proc_pidpath' answers, the +// kernel's record of the file it mapped (`proc_info', PROC_PIDPATHINFO); +// where that is refused, `executable_path=' from the vector this kernel passes +// after the environment, which is the name the kernel was given. +// 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 okm_long n = okm::sys(okm::nr_proc_info, 2 /* PROC_INFO_CALL_PIDINFO */, + okm::sys(okm::nr_getpid), 11 /* PROC_PIDPATHINFO */, 0, + reinterpret_cast(buf), sizeof buf); + if (!okm::failed(n) && n > 0) { + kal_uintptr len = 0; while (len < sizeof buf && buf[len] != '\0') ++len; + if (len > 0) return give(buf, len, out, cap); + } + if (okm::g_envp != nullptr) { + char** a = okm::g_envp; + while (*a) ++a; + for (++a; *a; ++a) { + const char key[] = "executable_path="; + kal_uintptr i = 0; + while (key[i] != '\0' && (*a)[i] == key[i]) ++i; + if (key[i] == '\0') return give(*a + i, okm::length(*a + i), 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("macos", 5, out, cap); +} + } diff --git a/src/exec.cpp b/src/exec.cpp index 83f0773..d8609be 100644 --- a/src/exec.cpp +++ b/src/exec.cpp @@ -169,6 +169,22 @@ int kal_exec_publish(void* p, kal_uintptr size) { return kal_ok; } +// PART OF A REGION (0.17): a run of whole pages made executable, the rest left +// writable. A part that is not whole pages is refused rather than rounded. +int kal_exec_publish_part(void* region, kal_uintptr offset, kal_uintptr size) { + const okm_uptr g = granularity(); + if (region == nullptr || size == 0 || (offset & (g - 1)) != 0 || (size & (g - 1)) != 0 + || (reinterpret_cast(region) & (g - 1)) != 0) + return kal_err_invalid; + const okm_long r = okm::sys(okm::nr_mprotect, + reinterpret_cast(static_cast(region) + offset), + static_cast(size), okm::prot_read | okm::prot_exec); + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + +// The page this kernel protects in, which is also what it reserves in. +kal_uintptr kal_exec_granularity(void) { return granularity(); } + void kal_exec_free(void* p, kal_uintptr size) { if (p == nullptr || size == 0) return; const okm_uptr bytes = round_up(static_cast(size), granularity()); diff --git a/src/fs.cpp b/src/fs.cpp index 796b2ba..dfca409 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -146,6 +146,12 @@ 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 the formats + // whose names this kernel keeps: `unlink' drops the entry and the file + // lives on until its last descriptor closes. A network volume's client + // renames a held file to a hidden name instead, so the directory that held + // it is not empty until the close, and the position is not claimed there. + const kal_uintptr local = conservative | KAL_FS_PROP_REMOVE_OPEN; if (fd < 0) return 0; okm::kstatfs64 sf{}; @@ -162,9 +168,13 @@ kal_uintptr volume_props(int fd) { // Nodes whose content is another name, on a format that stores a mode. // This is the ordinary volume of this system. + // + // Both implement `renameatx_np' with RENAME_EXCL (0.17), and both store a + // socket bound to a name. if (named("apfs") || named("hfs")) - return conservative | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS - | KAL_FS_PROP_EXECUTABLE; + return local | KAL_FS_PROP_LINKS | KAL_FS_PROP_MAKE_LINKS + | KAL_FS_PROP_EXECUTABLE + | KAL_FS_PROP_RENAME_NO_REPLACE | KAL_FS_PROP_LOCAL_ENDPOINT; // The automounter's own namespace: links, and no mode of its own. if (named("autofs")) @@ -179,7 +189,7 @@ kal_uintptr volume_props(int fd) { // The FAT family stores neither a case distinction nor a node that names // another, nor a mode. `symlinkat' on such a volume reports a refusal, and // this is where a caller learns it before it tries. - if (named("msdos") || named("exfat")) return conservative; + if (named("msdos") || named("exfat")) return local; return conservative; } @@ -659,6 +669,25 @@ int kal_fs_rename(kal_dir from, const char* a, kal_uintptr alen, return okm::failed(r) ? okm::translate(r) : kal_ok; } +// A RENAME THAT DOES NOT REPLACE (0.17): `renameatx_np' with RENAME_EXCL, the +// kernel's own test-and-rename. A format that does not implement the flag +// answers ENOTSUP, which is a volume that cannot. +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 = okm::unpack(from.h), t2 = okm::unpack(to.h); + if (f < 0 || t2 < 0 || !okm::acceptable(a, alen) || !okm::acceptable(b, blen)) + return kal_err_invalid; + if ((volume_props(t2) & KAL_FS_PROP_RENAME_NO_REPLACE) == 0) return kal_err_not_supported; + okm::terminated ta(a, alen), tb(b, blen); + if (!ta.ok || !tb.ok) return kal_err_invalid; + const okm_long r = okm::sys(okm::nr_renameatx_np, f, reinterpret_cast(ta.buf), + t2, reinterpret_cast(tb.buf), 0x4 /* RENAME_EXCL */); + if (r == -45 /* ENOTSUP */) return kal_err_not_supported; + return okm::failed(r) ? okm::translate(r) : kal_ok; +} + int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { const int fd = okm::unpack(d.h); if (fd < 0 || iter == nullptr) return kal_err_invalid; @@ -720,6 +749,15 @@ int kal_fs_list_next(kal_dir d, kal_uintptr* iter, // the position reports it in advance, which no operation could. kal_uintptr kal_fs_props(kal_dir d) { return volume_props(okm::unpack(d.h)); } +} // extern "C" (reopened below) + +namespace okm { +// For openkal.net's rendezvous at a name. +kal_uintptr volume_props_of(int fd) { return volume_props(fd); } +} // namespace okm + +extern "C" { + // Nodes whose content is another name. int kal_fs_link_create(kal_dir base, const char* name, kal_uintptr len, const char* target, kal_uintptr target_len, diff --git a/src/net.cpp b/src/net.cpp index f60f12a..692fa67 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -46,8 +46,64 @@ int report_address(okm_long call, int fd, kal_endpoint* out) { } // namespace +namespace okm { kal_uintptr volume_props_of(int fd); } // fs.cpp + +namespace { +// A RENDEZVOUS AT A NAME (0.17). This kernel binds a local socket to a path and +// has no call that binds beneath a descriptor; it does have a working +// directory per THREAD (`__pthread_fchdir'), so the name is bound relative to +// the directory the caller holds by entering it on this thread alone and +// leaving it again --- no other context's names change meaning meanwhile. The +// address holds 104 bytes. +int at_name(kal_dir base, const char* name, kal_uintptr len, bool listen, kal_uintptr& out) { + const int dir = okm::unpack(base.h); + if (dir < 0 || len == 0 || len > 103 || !okm::acceptable(name, len)) return kal_err_invalid; + for (kal_uintptr i = 0; i < len; ++i) if (name[i] == '/') return kal_err_invalid; + if ((okm::volume_props_of(dir) & KAL_FS_PROP_LOCAL_ENDPOINT) == 0) return kal_err_not_supported; + unsigned char addr[106] = {}; + addr[0] = static_cast(2 + len + 1); // sun_len + addr[1] = 1; // AF_UNIX + for (kal_uintptr i = 0; i < len; ++i) addr[2 + i] = static_cast(name[i]); + const okm_long fd = okm::sys(okm::nr_socket, 1 /* AF_UNIX */, okm::sock_stream, 0); + if (okm::failed(fd)) return okm::translate(fd); + close_on_exec(fd); + if (const okm_long r = okm::sys(okm::nr_pthread_fchdir, dir); okm::failed(r)) { + okm::sys(okm::nr_close, fd); + return okm::translate(r); + } + okm_long r = 0; + if (listen) { + r = okm::sys(okm::nr_bind, fd, reinterpret_cast(addr), addr[0]); + } else { + do r = okm::sys(okm::nr_connect, fd, reinterpret_cast(addr), addr[0]); + while (okm::interrupted(r)); + } + okm::sys(okm::nr_pthread_fchdir, -1); // back to the process's directory + if (!okm::failed(r) && listen) r = okm::sys(okm::nr_listen, fd, 128); + if (okm::failed(r)) { + okm::sys(okm::nr_close, fd); + if (listen && r == -48 /* EADDRINUSE */) return kal_err_exists; + if (!listen && (r == -2 /* ENOENT */ || r == -61 /* ECONNREFUSED */)) return kal_err_not_found; + return okm::translate(r); + } + out = okm::pack(static_cast(fd)); + if (out == 0) { okm::sys(okm::nr_close, fd); return kal_err_no_memory; } + return kal_ok; +} +} // namespace + extern "C" { +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; + return at_name(base, name, len, true, out->h); +} + +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; + return at_name(base, name, len, false, out->h); +} + int kal_net_connect(const kal_endpoint* to, kal_net_conn* out) { if (to == nullptr || out == nullptr) return kal_err_invalid; const okm_long family = okm::family_of(*to); diff --git a/src/process.cpp b/src/process.cpp index d7dcb97..4a1723f 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -3,6 +3,8 @@ #include #include +namespace okm { extern bool g_pipe_ignored_here; } + // A program image that has been started. // // This system has no operation that starts a program relative to a directory, @@ -326,6 +328,149 @@ extern "C" { // ⇒ 0.11 gives the caller a second directory, and both implementations now enter // the one the caller named. The divergence is gone because the thing that caused // it --- a property nobody had declared --- is declared. +} // extern "C" (reopened below) + +// --- the handles of this interface (0.17) ------------------------------------ +// +// A started program's handle is its identifier. Since 0.17 two more kinds are +// told apart by the top byte: this program itself, and a program found by its +// identifier. This kernel has no descriptor for a program, so the second +// carries, above the identifier, twenty-four bits of the moment the program +// started --- what tells the program found apart from a later one given the +// same number (process.h: "records started_ns at the open"). +namespace { +constexpr unsigned kind_child = 0, kind_self = 1, kind_opened = 2; +inline unsigned handle_kind(kal_process h) { return static_cast(h.h >> 56); } +inline okm_long handle_pid(kal_process h) { return static_cast(h.h & 0xffffffffu); } + +// `proc_bsdinfo', as `proc_info' (PROC_PIDTBSDINFO) writes it. +struct bsdinfo { + okm_u32 flags, status, xstatus, pid, ppid, uid, gid, ruid, rgid, svuid, svgid, rfu; + char comm[16]; char name[32]; + okm_u32 nfiles, pgid, pjobc, tdev, tpgid; int nice; + okm_u64 start_sec, start_usec; +}; +static_assert(sizeof(bsdinfo) == 136, "proc_bsdinfo"); + +bool bsd(okm_long pid, bsdinfo& out) { + const okm_long r = okm::sys(okm::nr_proc_info, 2 /* PROC_INFO_CALL_PIDINFO */, pid, + 3 /* PROC_PIDTBSDINFO */, 0, reinterpret_cast(&out), + sizeof out); + return !okm::failed(r) && r == static_cast(sizeof out); +} + +okm_u32 start_mark(const bsdinfo& b) { + const okm_u64 us = b.start_sec * 1000000ull + b.start_usec; + return static_cast((us ^ (us >> 24) ^ (us >> 48)) & 0xffffffu); +} + +// The kernel reports processor time in its own ticks, and this is the ratio to +// nanoseconds (`mach_timebase_info', a Mach trap): 125/3 on the processors this +// system now runs on, 1/1 on the earlier ones. +void timebase(okm_u32& numer, okm_u32& denom) { + struct { okm_u32 numer, denom; } tb{1, 1}; +#if defined(__aarch64__) + register okm_long x0 __asm__("x0") = reinterpret_cast(&tb); + register okm_long x16 __asm__("x16") = -89; + __asm__ __volatile__("svc #0x80" : "+r"(x0) : "r"(x16) : "memory", "cc", "x1"); +#else + okm_long r; + __asm__ __volatile__("syscall" : "=a"(r) : "a"(0x1000000 + 89), "D"(&tb) : "rcx", "r11", "memory"); +#endif + numer = tb.numer ? tb.numer : 1; denom = tb.denom ? tb.denom : 1; +} + +bool cpu_ns(okm_long pid, okm_u64& out) { + alignas(8) unsigned char ri[400] = {}; // rusage_info_v0 and some + const okm_long r = okm::sys(okm::nr_proc_info, 9 /* PROC_INFO_CALL_PIDRUSAGE */, pid, + 0 /* RUSAGE_INFO_V0 */, 0, reinterpret_cast(ri), 0); + if (okm::failed(r)) return false; + okm_u64 user = 0, sys = 0; + okm::copy(reinterpret_cast(&user), reinterpret_cast(ri + 16), 8); + okm::copy(reinterpret_cast(&sys), reinterpret_cast(ri + 24), 8); + okm_u32 n = 1, d = 1; + timebase(n, d); + out = (user + sys) / d * n + (user + sys) % d * n / d; + return true; +} + +bool has_ended(kal_process h) { + if (handle_kind(h) == kind_self) return false; + bsdinfo b{}; + if (handle_kind(h) == kind_opened) { + if (!bsd(handle_pid(h), b)) return true; + if (b.status == 5 /* SZOMB */) return true; + return start_mark(b) != static_cast((h.h >> 32) & 0xffffffu); + } + alignas(8) unsigned char info[128] = {}; + const okm_long r = okm::sys(okm::nr_waitid, 1 /* P_PID */, handle_pid(h), + reinterpret_cast(info), + 0x4 | 0x1 | 0x20 /* WEXITED | WNOHANG | WNOWAIT */); + if (r == -10 /* ECHILD */) return true; + if (okm::failed(r)) return false; + int si_pid = 0; + okm::copy(reinterpret_cast(&si_pid), reinterpret_cast(info + 8), sizeof si_pid); + return si_pid != 0; +} + +// EVERY ENVIRONMENT ENTRY IS `NAME=value' (0.17, process.h). +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; +} +} // namespace + +extern "C" { + +int kal_process_self(kal_process* out) { + if (out == nullptr) return kal_err_invalid; + *out = kal_process{ (static_cast(kind_self) << 56) + | static_cast(okm::sys(okm::nr_getpid)) }; + return kal_ok; +} + +int kal_process_open_id(kal_u64 id, kal_process* out) { + if (out == nullptr || id == 0) return kal_err_invalid; + if (id > 0x7fffffffu) return kal_err_not_found; + bsdinfo b{}; + if (!bsd(static_cast(id), b)) { + const okm_long probe = okm::sys(okm::nr_kill, static_cast(id), 0); + return probe == -1 /* EPERM */ ? kal_err_permission : kal_err_not_found; + } + if (b.status == 5 /* SZOMB */) return kal_err_not_found; + *out = kal_process{ (static_cast(kind_opened) << 56) + | (static_cast(start_mark(b)) << 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 okm_long pid = handle_pid(p); + v.id = static_cast(pid); + v.present = KAL_PINFO_ID | KAL_PINFO_ENDED; + const bool ended = has_ended(p); + v.ended = ended ? 1u : 0u; + bsdinfo b{}; + if (!ended && (wanted & KAL_PINFO_STARTED) && bsd(pid, b)) { + v.started_ns = b.start_sec * 1000000000ull + b.start_usec * 1000ull; + v.present |= KAL_PINFO_STARTED; + } + okm_u64 c = 0; + if (!ended && (wanted & KAL_PINFO_CPU) && cpu_ns(pid, c)) { v.cpu_ns = c; v.present |= KAL_PINFO_CPU; } + const kal_u32 n = self < sizeof v ? self : static_cast(sizeof v); + okm::copy(reinterpret_cast(out), reinterpret_cast(&v), n); + return kal_ok; +} + 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, @@ -347,6 +492,8 @@ int kal_process_spawn(const kal_spawn* how, // live context to notice. Composing it would move the defect from "refused" // to "works except when it matters". if (how->flags != 0) return kal_err_not_supported; + if (envc > 0 && (envp == nullptr || envp_lens == nullptr)) return kal_err_invalid; + if (!entries_well_formed(envp, envp_lens, envc)) return kal_err_invalid; okm::terminated p(path, path_len); if (!p.ok) return kal_err_invalid; @@ -477,6 +624,13 @@ int kal_process_spawn(const kal_spawn* how, if (unit) okm::sys(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 (okm::g_pipe_ignored_here) { + struct { void (*handler)(int); void* tramp; unsigned int mask; int flags; } standard{}; + okm::sys(46 /* sigaction */, 13 /* SIGPIPE */, reinterpret_cast(&standard), 0); + } + const okm_long why = okm::sys(okm::nr_execve, reinterpret_cast(whole), reinterpret_cast(args.slots), reinterpret_cast(envs.slots)); @@ -549,6 +703,8 @@ void kal_process_channel_close(kal_stream s) { // One program, whatever unit it is in --- the unit has its own operation below, 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) return kal_err_not_supported; int st = 0; for (;;) { const okm_long r = okm::sys(okm::nr_wait4, static_cast(h.h), @@ -574,6 +730,8 @@ int kal_process_wait(kal_process h, int* status, int* terminated_by_environment) // so this one's meaning never turns on how the program was started. 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) return kal_err_not_supported; const okm_long r = okm::sys(okm::nr_kill, static_cast(h.h), 15 /* SIGTERM */); return okm::failed(r) ? okm::translate(r) : kal_ok; } @@ -614,10 +772,18 @@ int g_stop_armed = 0; constexpr okm_long nr_sigaction = 46; // What the kernel enters. Its arguments are (handler, infostyle, sig, siginfo, -// ucontext) in x0..x4; it calls the handler with the last three and then asks -// the kernel to restore the interrupted context. +// ucontext, token) in x0..x5; it calls the handler with sig, siginfo and +// ucontext and then asks the kernel to restore the interrupted context. +// +// THE TOKEN IS PASSED BACK (0.15). The kernel validates `sigreturn' against a +// per-thread token it hands the trampoline in x5, and validation is switched +// on for the process the first time anything installs a disposition through +// the C library. Until then a trampoline that passed nothing worked --- which +// is how this one shipped --- and afterwards `sigreturn' refused, and the +// `brk' below ended the program with SIGTRAP: measured, a program that called +// `signal(SIGINT, SIG_IGN)' before arming the word died on its first SIGTERM. // -// x19 AND x20 ARE USED WITHOUT BEING SAVED, and that is correct here rather +// x19, x20 AND x21 ARE USED WITHOUT BEING SAVED, and that is correct here rather // than sloppy: this function does not return to its caller. `sigreturn' restores // the whole of the interrupted context, callee-saved registers included, so the // values these two held belong to a frame the kernel is about to reinstate. @@ -627,6 +793,7 @@ asm(".globl _okm_sigtramp\n" "_okm_sigtramp:\n" " mov x19, x1\n" // infostyle " mov x20, x4\n" // ucontext + " mov x21, x5\n" // token " mov x8, x0\n" // handler " mov x0, x2\n" // sig " mov x1, x3\n" // siginfo @@ -634,11 +801,17 @@ asm(".globl _okm_sigtramp\n" " blr x8\n" " mov x0, x20\n" // ucontext " mov x1, x19\n" // infostyle + " mov x2, x21\n" // token " mov x16, #184\n" // SYS_sigreturn " svc #0x80\n" " brk #1\n"); // sigreturn does not come back -void stop_handler(int) { +int g_stop_signal = 0; // which request came first, for kal_process_end_as_requested + +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); // Woken through the same operation `kal_task_wake' performs, issued as the // raw call because a handler may not enter code that takes a lock. Waking @@ -664,6 +837,14 @@ struct macos_sigaction { // nothing' in its exact form: the program would ask whether its end had been // requested, be told no, and go on being told no after it had been. bool arm_one(int signo) { + // A REQUEST THE STARTER ARRANGED NOT TO DELIVER STAYS UNDELIVERED (0.17, + // the take-over rule): an inherited SIG_IGN is left as it is. + { + struct { void (*handler)(int); unsigned int mask; int flags; } old{}; + if (!okm::failed(okm::sys(nr_sigaction, signo, 0, reinterpret_cast(&old))) + && old.handler == reinterpret_cast(1)) + return true; + } macos_sigaction act{}; act.handler = &stop_handler; act.tramp = reinterpret_cast(&okm_sigtramp); @@ -704,6 +885,26 @@ const kal_u32* kal_process_stop_requested(void) { #endif } +// ENDS AS THE REQUEST WOULD HAVE ENDED THE PROGRAM (0.17): the request's +// default treatment restored, the request unblocked and sent again; a program +// still here a moment later ends with the status a shell gives one so ended. +int kal_process_end_as_requested(void) { + if (__atomic_load_n(&g_stop_word, __ATOMIC_ACQUIRE) == 0) return kal_err_invalid; +#if defined(__aarch64__) + int signo = __atomic_load_n(&g_stop_signal, __ATOMIC_RELAXED); + if (signo == 0) signo = 15; + macos_sigaction act{}; // SIG_DFL, no trampoline needed + okm::sys(nr_sigaction, signo, reinterpret_cast(&act), 0); + unsigned int set = 1u << (signo - 1); + okm::sys(okm::nr_sigprocmask, 2 /* SIG_UNBLOCK */, reinterpret_cast(&set), 0); + // A signal a program sends itself, unblocked, is delivered before the call + // returns; the exit below is reached only if it was not. + okm::sys(okm::nr_kill, okm::sys(okm::nr_getpid), signo); + okm::sys(okm::nr_exit, 128 + signo); +#endif + for (;;) { } +} + // This program itself joins or forms a unit --- what `kal_spawn.job' cannot say, // because that places a program the caller STARTS and a copy wishing to lead a // unit must say so about ITSELF before it replaces itself. @@ -736,7 +937,7 @@ 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) { } // no kind of handle here holds a resource // Starting a program whose lifetime is bound to this one's. Version 0.10. // @@ -751,6 +952,7 @@ kal_uintptr kal_process_props(void) { return | KAL_PROCESS_PROP_EXIT_STATUS | KAL_PROCESS_PROP_CHANNEL | KAL_PROCESS_PROP_GRANT_DIR | KAL_PROCESS_PROP_JOB + | KAL_PROCESS_PROP_OPEN_ID #if defined(__aarch64__) // AND IT AGREES WITH `kal_process_stop_requested', WHICH IS A REQUIREMENT // AND NOT A COURTESY: the header defines null there as the absence this diff --git a/src/sys.h b/src/sys.h index 3f24e39..e9b78fc 100644 --- a/src/sys.h +++ b/src/sys.h @@ -194,6 +194,10 @@ enum : okm_long { nr_getrlimit = 194, // openkal 0.13: whether a node may be started nr_fchmodat = 467, + // openkal 0.17: a rename that does not replace, a program's information, + // how one ended, the request reproduced, and a thread's own directory. + nr_renameatx_np = 488, nr_proc_info = 336, nr_waitid = 173, + nr_sigprocmask = 48, nr_pthread_fchdir = 349, nr_ulock_wait = 515, nr_ulock_wake = 516, // The socket calls, and the one that reports readiness. diff --git a/src/terminal.cpp b/src/terminal.cpp index d319389..a827140 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -75,6 +75,8 @@ kal_uintptr mode_of(const oktermios& t) { if ((t.lflag & (t_isig | t_iexten)) == 0 && (t.iflag & t_ixon) == 0) m |= KAL_TERM_PASS_CONTROL; if ((t.iflag & kAlters) == 0) m |= KAL_TERM_PASS_INPUT; + // This system's terminals obey ECMA-48 whatever is asked (0.17). + m |= KAL_TERM_SEQUENCES; return m; } diff --git a/tests/sigpipe_not_passed_on.cpp b/tests/sigpipe_not_passed_on.cpp new file mode 100644 index 0000000..8b83a82 --- /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, K8) and is not passed +// on (0.15): 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-macos: SIGPIPE ignored here, not passed on\n"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/stop_word_and_sigpipe.cpp b/tests/stop_word_and_sigpipe.cpp new file mode 100644 index 0000000..6a86c39 --- /dev/null +++ b/tests/stop_word_and_sigpipe.cpp @@ -0,0 +1,65 @@ +// The stop word given back, and a closed stream reported rather than fatal +// (openkal 0.17, process.h; K2, K8). +// +// 1. (tests/stop_word_ignored.cpp: an ignored request stays ignored, K2); +// 2. ending as requested is reported to the starter as the request (G4); +// 3. writing to a stream whose far end is gone is an error the program is +// told, not SIGPIPE ending it (K8). +#include +#include +#include +#include +#include +import openkal.types; +import openkal.env; +import openkal.stream; +import openkal.process; + +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; + } + + { + kal_stream mine{}, theirs{}; + check(kal_process_channel(&mine, &theirs) == kal_ok, "a channel is made"); + kal_process_channel_close(mine); + const kal_intptr r = kal_stream_write(theirs, "x", 1); + check(r < 0, "writing to a stream whose reader is gone is reported, and the program lives"); + kal_process_channel_close(theirs); + } + + const kal_u32* word = kal_process_stop_requested(); +#if defined(__aarch64__) + check(word != nullptr, "the stop word is armed"); + char self[4096] = {}; + const kal_intptr n = kal_env_image(self, sizeof self - 1); + check(n > 0, "the program's own image is named"); + const pid_t child = ::fork(); + if (child == 0) { + ::execl(self, 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"); +#else + (void)word; (void)argv; +#endif + if (failures == 0) std::printf("openkal-macos: the stop word given back, and a closed stream reported\n"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/stop_word_ignored.cpp b/tests/stop_word_ignored.cpp new file mode 100644 index 0000000..9b00af3 --- /dev/null +++ b/tests/stop_word_ignored.cpp @@ -0,0 +1,23 @@ +// A request the starter ignored stays ignored once the stop word is armed +// (openkal 0.17, the take-over rule; K2). +#include +#include +#include +import openkal.types; +import openkal.process; + +int main() { +#if defined(__aarch64__) + std::signal(SIGINT, SIG_IGN); + const kal_u32* word = kal_process_stop_requested(); + if (word == nullptr) { std::printf("FAIL: the stop word is armed\n"); return 1; } + ::kill(::getpid(), SIGINT); + ::usleep(20000); + if (*word != 0) { std::printf("FAIL: an ignored request was delivered\n"); return 1; } + ::kill(::getpid(), SIGTERM); + for (int i = 0; i < 1000 && *word == 0; ++i) ::usleep(1000); + if (*word == 0) { std::printf("FAIL: a request after arming sets the word\n"); return 1; } +#endif + std::printf("openkal-macos: an ignored request stays ignored\n"); + return 0; +}