diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f268c8..2c41a01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -615,22 +615,41 @@ jobs: cxx_runtime = "host-coupled" TOML sed -i 's/^ //' "$d/mcpp.toml" - # ⭐ `symlinkat` IS THE OPERATION AND THE PROBE PRINTS ITS OWN NUMBER. + # ⚠️⚠️ THE OPERATION THIS PROBE NAMES MUST STILL BE ABSENT, AND THE + # ONE IT NAMED STOPPED BEING SO. # - # openkal has no atom that creates a link --- the manifest and README - # both record that --- so it reaches the default arm rather than a case - # with a decision of its own. The number is printed by the probe rather - # than computed here, so the expectation comes from the same headers - # the dispatcher was compiled with; a number worked out on the host - # would be answering for the host's C library. + # It was `symlinkat`, chosen because openkal had no atom that creates a + # link. openkal 0.9 added `kal_fs_link_create`, this dispatcher gained + # a case for it, and the probe --- unchanged, still green in intent --- + # began exercising an operation that now SUCCEEDS. It reported zero + # diagnostics for a hundred attempts, which is the same reading a + # broken diagnostic channel gives. + # + # ⭐ SO THE ABSENCE IS NOW ASSERTED RATHER THAN ASSUMED. `mknodat` + # creates a device node, which openkal does not express and is not + # going to; if a case for it ever appears, the grep below fails and + # says to pick another operation --- instead of this step passing + # while measuring nothing. + if grep -q 'SYS_mknodat' port/src/okm_syscall.c; then + echo "::error::the dispatcher now handles mknodat, so this probe no" + echo "::error::longer exercises an absent operation. Pick another" + echo "::error::operation openkal has no atom for and name it here." + exit 1 + fi + # The number is printed by the probe rather than computed here, so the + # expectation comes from the same headers the dispatcher was compiled + # with; a number worked out on the host would answer for the host's C + # library. printf '%s\n' \ '#include ' \ + '#include ' \ '#include ' \ '#include ' \ '#include ' \ 'int main(void) {' \ - ' for (int i = 0; i < 100; i++) (void)!symlinkat("a", AT_FDCWD, "b");' \ - ' printf("%ld\n", (long)SYS_symlinkat);' \ + ' for (int i = 0; i < 100; i++)' \ + ' (void)!mknodat(AT_FDCWD, "node", S_IFIFO | 0600, 0);' \ + ' printf("%ld\n", (long)SYS_mknodat);' \ ' return 0;' \ '}' > "$d/src/main.c" ( cd "$d" && mcpp build --toolchain '${{ matrix.toolchain }}' ) diff --git a/.gitignore b/.gitignore index a82c0e2..269ec14 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ crash.log # What the object-level checks in continuous integration write beside the # sources they examine. syms.txt + +# A working tree of the specification or of an implementation placed beside the +# sources. No trailing slash: the pattern must match a symbolic link as well. +.spec +.impl diff --git a/README.md b/README.md index 9d45c16..31ac86a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the claim can be checked rather than repeated. ```toml [dependencies] -openkal-musl = "0.6.0" +openkal-musl = "0.7.0" ``` It names no implementation and no platform: a C library is the one consumer that @@ -108,8 +108,10 @@ answer that leaves a program wrong without telling it. | memory protection | `mprotect` reports `ENOSYS` | openkal has no operation upon a mapping's protection. musl asks for a guard page below a thread's stack and proceeds without one when told this, so the honest answer is also the one it is prepared for. | | out-of-band data | `MSG_OOB`, `MSG_PEEK`, and `POLLPRI` are never reported and `recv` refuses the flags | openkal's transfer operations move bytes and have no second channel and no non-destructive read. | | readiness *sets* | `epoll` is not built at all, so the link names it | a set held by the environment is a facility of one kernel rather than a capability. `poll` and `select` ask each descriptor in turn, which is what an interface without a set permits. | -| symbolic links | `symlink` reports `ENOSYS`; `readlink` reports `EINVAL` for a name that is not one and `ENOSYS` for one that is | `SURFACE.txt` has no operation that creates or reads a link. It *does* have `kal_node_link` and `KAL_FS_PROP_LINKS`, so an implementation can report a link it encounters and cannot make one; the asymmetry is the specification's and is recorded rather than worked around. | -| ownership and permission bits | `chmod` and `chown` report `ENOSYS`; `stat` reports a mode assembled from what openkal knows | `kal_node_info` carries `writable` — one boolean, not a mode word — and `kal_fs_open_file` takes flags rather than a mode. Mapping the owner-write bit onto it would make `chmod(0600)` succeed and `stat` report something else, which is the shape this port exists to avoid. | +| ~~symbolic links~~ | **answered since 0.7.0** — `symlink`, `readlink`, and `stat`/`lstat` telling the two questions apart | openkal 0.9 carries `kal_fs_link_create` and `kal_fs_link_read` as operations of `openkal.fs`, and `kal_fs_props` takes the directory, so this port asks whether the volume has such nodes before it uses them. Where it does not, the refusal is what the enquiry already said. | +| permission bits | `chmod` reports `ENOSYS`; `stat` reports a mode assembled from what openkal knows | `kal_node_info` carries `writable` — one boolean, not a mode word — and `kal_fs_open` takes flags rather than a mode. Mapping the owner-write bit onto it would make `chmod(0600)` succeed and `stat` report something else, which is the shape this port exists to avoid. | +| the identity of a node | `st_dev` and `st_ino` are the implementation's answer where it has one, and **zero for both where it has none** | ⚠️ They were the constants 0 and 1, so every file compared equal to every other: `std::filesystem::equivalent` on two separately created files answered `true` **with no error**. openkal 0.9 carries an identity and reports whether it knows one; a caller must not read two zeroes as sameness, which is why nothing is invented for an implementation that cannot distinguish nodes. | +| ownership | `chown` reports `ENOSYS`; `stat` reports 1000 for both | as the row above: a capability-oriented environment has no principal for an owner to name. | | a mode given at creation | `open(…, O_CREAT, 0600)` and `mkdir(path, 0700)` **succeed** and `stat` afterwards reports 0666 and 0777 | the row above, in the one place where it does not read as a refusal. openkal opens a file for a purpose and not for an audience, so the argument has nowhere to go. Refusing every mode but the one `stat` will report would refuse nearly every program; what a caller can rely on instead is stated below. | | entropy | `getrandom` reports `ENOSYS` where the backend declines `openkal.random` | openkal has no source of one to require, and this port does not invent one. The allocator's cookie and the stack canary are derived from the clock and from an address; neither is a security property here. | | a signal delivered anywhere | `raise` and `kill` perform a signal's **default action** and nothing else: terminating signals end the program, ignored ones succeed, stopping ones report `ENOSYS`, and musl's own three (32, 33, 34) are refused, so `pthread_cancel` reports `ENOSYS` | there is no delivery, so there is no handler to reach; what remains of a signal is what it does when no handler exists. `abort` reaches `kal_abort`, which raises the signal on Linux and ends with a distinguished status elsewhere — a parent can tell an abnormal end from an ordinary one on every system. | @@ -141,10 +143,30 @@ was given would not be confined by having been given it. `/a/b/../c` is therefore reduced to `/a/c` before openkal sees it, which is what the program means in every case except one that passes through a symbolic link. +**`O_NOFOLLOW` is answered by an enquiry, not by an opening.** openkal states +that opening resolves and offers no form that declines to, deliberately: a +program that opens a link in order to read its bytes is asking what +`kal_fs_link_read` answers. So this port asks `kal_fs_info` with +`KAL_FS_NO_RESOLVE` first and reports `ELOOP` when the name is a link, which is +what POSIX says and what a caller passing the flag is distinguishing. Answering +`ENOENT` instead — which is what resolving a link to an absent target produces — +is a different answer to a different question, and libc++'s `remove_all` reads it +as "the entry has already gone" and leaves the tree standing. + **The tables are bounded.** A program may hold 1024 descriptors and 512 open descriptions; beyond that it is told so. Allocating the tables instead would place them on the allocator, and the allocator obtains its memory through them. +⚠️ **And a program may have started 256 programs it has not waited for.** An +entry is taken when a program is started and released when it is waited for, +which is what a process table is; a program that never waits holds entries for +ever, and the next start reports `EAGAIN` — which is what POSIX says `fork` +does when the table is full. This bound is stated here because it was not, and +a caller that met it saw a failure on an operation with no evident relation to +the ones that caused it: measured, the sixty-fifth `posix_spawn` of a program +that waited for none, and of one that polled each once with `WNOHANG` and did +not come back. + ## Asking which operation was missing `ENOSYS` says that a facility is not here. It does not say which one, and until diff --git a/examples/posix/src/main.c b/examples/posix/src/main.c index 4b47ebf..a1e6d78 100644 --- a/examples/posix/src/main.c +++ b/examples/posix/src/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -164,6 +165,177 @@ int main(int argc, char **argv, char **envp) { failures += 2; } + /* ⭐⭐ THE DISPOSITION OF A SIGNAL IS TOUCHED, WHICH NOTHING HERE DID. + * + * This file had thirty-six observations and three of them were about + * `abort'. It contained no call to `signal' or `sigaction' anywhere --- so + * it examined whether `abort' ENDS the program and never whether a program + * may ASK what a signal is set to. A defect that killed any program doing + * the second passed every one of the thirty-six. + * + * ⚠️ ALL THREE FORMS, AND SIGABRT AMONG THEM. The C library takes a lock for + * any change to that one disposition and blocks signals to take it, so + * SIGABRT reaches code the others do not --- and the enquiry, which changes + * nothing, reached it too. Two of the three forms below would have passed + * while the third killed the process. */ + { + int survived = 1; + for (int sig = 1; sig < 32; sig++) { + if (sig == SIGKILL || sig == SIGSTOP) continue; + struct sigaction seen; + memset(&seen, 0, sizeof seen); + /* An enquiry, which changes nothing. */ + const int q = sigaction(sig, NULL, &seen); + if (q != 0 && errno != ENOSYS) survived = 0; + /* Ignoring, which every environment can express. */ + errno = 0; + signal(sig, SIG_IGN); + if (errno != 0 && errno != ENOSYS) survived = 0; + /* A handler, which this environment cannot deliver and refuses. */ + errno = 0; + if (signal(sig, SIG_DFL) == SIG_ERR && errno != ENOSYS) survived = 0; + } + check(survived, "every signal's disposition may be read and written or refused"); + } + + /* Nodes whose content is another name, where the volume has them. */ + { + /* The names this block uses are its own: the file the earlier + * observations made has been removed by the time this runs, and a probe + * that depended on another probe's leftovers would report an absence as + * a defect. */ + { FILE *t = fopen("okm-link-target.tmp", "w"); if (t) { fputs("0123456789", t); fclose(t); } } + unlink("okm-probe-link"); + const int made = symlink("okm-link-target.tmp", "okm-probe-link"); + if (made == 0) { + char target[64] = { 0 }; + const ssize_t got = readlink("okm-probe-link", target, sizeof target - 1); + check(got == (ssize_t)strlen("okm-link-target.tmp") + && strcmp(target, "okm-link-target.tmp") == 0, + "a node's content reads back as it was written"); + + /* ⭐ THE OBSERVATION THE PORT MOST NEEDED. Asking resolves and + * opening resolves, so the two agree; asking with the flag reports + * the node itself. They disagreed, and a C++ library above reported + * a link where a caller would have reached a file. */ + struct stat followed, itself; + check(stat("okm-probe-link", &followed) == 0 && S_ISREG(followed.st_mode), + "stat resolves, and reports what the name finally refers to"); + check(lstat("okm-probe-link", &itself) == 0 && S_ISLNK(itself.st_mode), + "lstat reports the node itself"); + + /* ⭐⭐ AND THE THIRD QUESTION, WHICH IS NEITHER OF THOSE TWO. + * + * O_NOFOLLOW does not ask to open the link and does not ask to + * open its target: it asks `is this name a link?' and expects + * ELOOP when it is. openkal offers no opening that declines to + * resolve --- by design --- so this port resolved, and for a link + * to a name that is absent it answered ENOENT. + * + * ⚠️ THAT IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION, AND + * NOTHING NEARBY LOOKED WRONG. Every operation above still held. + * What failed was three layers up: libc++'s `remove_all' descends + * by opening each entry O_DIRECTORY|O_NOFOLLOW and reads ENOENT as + * `it is already gone', so it unlinked nothing and then reported + * ENOTEMPTY for a directory it had just declined to empty. The + * host toolchain removed the same tree. + * + * The answer comes from the enquiry openkal 0.9 added: ask about + * the name itself. Both cases are checked because they fail + * differently --- a live target resolved to a FILE and returned a + * descriptor, which is not an error at all. */ + int nf = open("okm-probe-link", O_RDONLY | O_NOFOLLOW); + check(nf < 0 && errno == ELOOP, + "opening a link with O_NOFOLLOW reports that it is a link"); + if (nf >= 0) close(nf); + + unlink("okm-probe-target-gone"); + unlink("okm-probe-dangling"); + if (symlink("okm-probe-target-gone", "okm-probe-dangling") == 0) { + nf = open("okm-probe-dangling", O_RDONLY | O_NOFOLLOW); + check(nf < 0 && errno == ELOOP, + "and does so for a link whose target is absent, rather than ENOENT"); + if (nf >= 0) close(nf); + check(unlink("okm-probe-dangling") == 0, + "a link whose target is absent is still removable"); + } + + unlink("okm-probe-link"); + } else if (errno == ENOSYS || errno == EPERM) { + printf("ok: this volume has no nodes that name others, which it reported\n"); + } else { + printf("FAIL: making a node that names another (errno %d)\n", errno); + failures++; + } + unlink("okm-link-target.tmp"); + } + + /* ⚠️ Two different files are two different files. `st_dev' and `st_ino' + * were constants, so every file compared equal to every other and a C++ + * library's `equivalent' answered true with no error. + * + * ⚠️⚠️ WHEN THIS FAILS, THE DEFECT IS USUALLY NOT IN THIS PACKAGE. This + * port copies the identity out of `kal_node_info' and puts zero there when + * the implementation does not report one --- which is permitted, and which + * makes every node compare equal to every other. So a failure here says + * "the openkal implementation beneath this one declined to report an + * identity", and the place to look is its `kal_fs_info'. + * + * Measured: it failed on Windows, and openkal-windows was reading a volume + * serial number the object manager had written and then discarding it, + * because the enquiry reported STATUS_BUFFER_OVERFLOW for a volume label + * that did not fit and the implementation read that as a failure. The + * conformance suite could not have said so: an implementation is allowed + * to decline the field, so the suite reports the observation as one it did + * not make. This is the criterion that notices, and it is two packages + * away from the defect. */ + { + struct stat x, y; + FILE *fx = fopen("okm-probe-x.tmp", "w"); if (fx) fclose(fx); + FILE *fy = fopen("okm-probe-y.tmp", "w"); if (fy) fclose(fy); + check(stat("okm-probe-x.tmp", &x) == 0 && stat("okm-probe-y.tmp", &y) == 0 + && !(x.st_dev == y.st_dev && x.st_ino == y.st_ino), + "two different files have different identities"); + unlink("okm-probe-x.tmp"); + unlink("okm-probe-y.tmp"); + } + + /* A value POSIX says cannot fail is not a negated error. */ + check(getpgrp() > 0, "the process group is a number and not a negated error"); + + /* The page is the machine's and not the build's. + * + * ⚠️⚠️ AND "POSITIVE POWER OF TWO" WAS TRUE OF THE VALUE THAT BROKE IT. + * This library took `kal_memory_granularity()' as its page size, and an + * implementation for a machine with no memory management unit answers ONE + * --- correctly, since nothing there needs rounding. One is positive and + * one is a power of two, so this assertion held while the allocator asked + * the environment for one-byte extents and the program stopped inside the + * first allocation that needed a new one. + * + * ⭐ SO THE CRITERION IS WHAT THE ALLOCATOR REQUIRES, NOT WHAT THE NUMBER + * LOOKS LIKE. A page smaller than this library's own quantum is not a page + * this library can use, whatever openkal reports. */ + { + const long page = sysconf(_SC_PAGESIZE); + check(page >= 4096 && (page & (page - 1)) == 0, + "the page size is a power of two no smaller than the allocator's quantum"); + + /* And the property the number exists to have. Several pages, written + * end to end: the allocation this library rounds to `page' and the + * memory it hands back are the same memory. */ + const size_t span = (size_t)page * 4 + 17; + unsigned char *big = malloc(span); + int whole = big != NULL; + if (big) { + for (size_t i = 0; i < span; i++) big[i] = (unsigned char)(i * 31u); + for (size_t i = 0; i < span; i++) + if (big[i] != (unsigned char)(i * 31u)) { whole = 0; break; } + free(big); + } + check(whole, "several pages are obtained in one allocation and every byte of it holds"); + } + printf("-- failures: %d --\n", failures); return failures ? 1 : 0; } diff --git a/mcpp.toml b/mcpp.toml index d93d72b..fa864c9 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.6.0" +version = "0.7.0" description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel." license = "Apache-2.0" @@ -15,7 +15,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-musl" [dependencies] -openkal = "0.8.0" +openkal = "0.9.0" # An ordinary consumer of openkal declares the specification and leaves the # choice of implementation to whoever builds the program, which is what the @@ -30,10 +30,10 @@ openkal = "0.8.0" # # The consequence for a program is that it names this package and nothing else. [target.'cfg(os = "linux")'.dependencies] -openkal-linux = { version = "0.6.0", features = ["standalone"] } +openkal-linux = { version = "0.7.0", features = ["standalone"] } [target.'cfg(os = "macos")'.dependencies] -openkal-macos = { version = "0.5.0", features = ["standalone"] } +openkal-macos = { version = "0.6.0", features = ["standalone"] } # ⚠️ FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. # @@ -44,7 +44,7 @@ openkal-macos = { version = "0.5.0", features = ["standalone"] } # runtime that receives control, and a C library configured for an environment # with no process to exit from. So this declares the implementation and stops. [target.'cfg(os = "none")'.dependencies] -openkal-opensbi = { version = "0.2.0", features = ["standalone"] } +openkal-opensbi = { version = "0.3.0", features = ["standalone"] } # ⭐ WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE. # @@ -69,7 +69,7 @@ openkal-opensbi = { version = "0.2.0", features = ["standalone"] } defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"] [target.'cfg(windows)'.dependencies] -openkal-windows = { version = "0.3.0", features = ["standalone"] } +openkal-windows = { version = "0.4.0", features = ["standalone"] } # The feature macros musl's own build establishes. # diff --git a/port/src/okm_fd.c b/port/src/okm_fd.c index c065119..3d813db 100644 --- a/port/src/okm_fd.c +++ b/port/src/okm_fd.c @@ -241,7 +241,11 @@ int okm_fd_get_cloexec(int fd) /* --- the supplied directories --------------------------------------------- */ -static struct { struct kal_dir dir; const char* name; size_t len; } g_pre[16]; +/* The names are held here rather than pointed at. openkal 0.9 copies a name + * into the caller's buffer instead of answering with a pointer into its own, + * which is what lets this library behave the same whether the implementation is + * linked into it, loaded beside it, or across a boundary. */ +static struct { struct kal_dir dir; char name[256]; size_t len; } g_pre[16]; static int g_npre; struct kal_dir okm_cwd_dir; @@ -288,9 +292,16 @@ void okm_table_init(void) const kal_uintptr n = okm_fs_preopen_count(); g_npre = 0; for (kal_uintptr i = 0; i < n && g_npre < 16; i++) { - struct kal_dir d; const char* nm = 0; kal_uintptr l = 0; - if (okm_fs_preopen(i, &d, &nm, &l) != kal_ok) continue; - g_pre[g_npre].dir = d; g_pre[g_npre].name = nm; g_pre[g_npre].len = l; + struct kal_dir d; kal_uintptr l = 0; + /* The name is copied into this layer's own storage. It answered with a + * pointer into the implementation's, which is meaningful only while the + * implementation shares this address space --- and this library is the + * one consumer that must behave the same whichever way it is reached. */ + if (okm_fs_preopen(i, &d, g_pre[g_npre].name, + sizeof g_pre[g_npre].name - 1, &l) != kal_ok) continue; + if (l >= sizeof g_pre[g_npre].name) continue; + g_pre[g_npre].name[l] = 0; + g_pre[g_npre].dir = d; g_pre[g_npre].len = l; g_npre++; } diff --git a/port/src/okm_net.c b/port/src/okm_net.c index 90c81e2..5fd4b35 100644 --- a/port/src/okm_net.c +++ b/port/src/okm_net.c @@ -370,7 +370,7 @@ static int adopt(struct kal_net_conn c, int flags) struct kal_file nof = { 0 }; struct kal_dir nod = { 0 }; - okm_fd_bind(fd, OKM_SOCKET, kal_net_stream(c), nof, nod, O_RDWR | flags); + okm_fd_bind(fd, OKM_SOCKET, kal_net_stream(c).h, nof, nod, O_RDWR | flags); struct okm_desc* nd = okm_desc_of(fd); nd->sock = slot; g_sock[slot].state = OKM_SOCK_CONN; @@ -469,7 +469,7 @@ int okm_sock_connect(int fd, const void* addr, unsigned len) s->state = OKM_SOCK_CONN; s->peer = ep; s->have_peer = 1; - d->stream = kal_net_stream(s->conn); + d->stream = kal_net_stream(s->conn).h; return 0; } @@ -566,9 +566,9 @@ long okm_sock_send(int fd, const void* buf, unsigned long len, int flags, if (e != kal_ok) return -okm_errno(e); s->state = OKM_SOCK_DGRAM; } - const struct kal_io_result io = kal_datagram_send_to(s->dg, buf, len, &to); - if (io.e != kal_ok) return -okm_errno(io.e); - return (long)io.n; + const kal_intptr io = kal_datagram_send_to(s->dg, buf, len, &to); + if (io < 0) return -okm_errno((int)-io); + return (long)io; } if (s->state != OKM_SOCK_CONN) return -ENOTCONN; @@ -577,9 +577,9 @@ long okm_sock_send(int fd, const void* buf, unsigned long len, int flags, if (ns) return okm_timed_write(d->stream, buf, len, ns); struct kal_stream st; st.h = d->stream; - const struct kal_io_result io = kal_stream_write(st, buf, len); - if (io.e != kal_ok) return io.n ? (long)io.n : -okm_errno(io.e); - return (long)io.n; + const kal_intptr io = kal_stream_write(st, buf, len); + if (io < 0) return -okm_errno((int)-io); + return (long)io; } long okm_sock_recv(int fd, void* buf, unsigned long len, int flags, @@ -613,16 +613,16 @@ long okm_sock_recv(int fd, void* buf, unsigned long len, int flags, s->pend_msg = 0; } else { const kal_u64 ns = bound_of(d, s, flags, 1); - struct kal_io_result io; + kal_intptr io; if (ns) { if (!kal_timeout_recv_from) return -ENOSYS; io = kal_timeout_recv_from(s->dg, buf, len, &from, ns); - if (io.e == kal_err_again) return -EAGAIN; + if (io == -kal_err_again) return -EAGAIN; } else { io = kal_datagram_recv_from(s->dg, buf, len, &from); } - if (io.e != kal_ok) return -okm_errno(io.e); - got = (unsigned long)io.n; + if (io < 0) return -okm_errno((int)-io); + got = (unsigned long)io; } if (addr && alen) from_endpoint(&from, addr, alen); return (long)got; @@ -639,9 +639,9 @@ long okm_sock_recv(int fd, void* buf, unsigned long len, int flags, if (ns) return okm_timed_read(d->stream, buf, len, ns); struct kal_stream st; st.h = d->stream; - const struct kal_io_result io = kal_stream_read(st, buf, len); - if (io.e != kal_ok) return -okm_errno(io.e); - return (long)io.n; + const kal_intptr io = kal_stream_read(st, buf, len); + if (io < 0) return -okm_errno((int)-io); + return (long)io; } /* --- options ---------------------------------------------------------------- @@ -774,11 +774,11 @@ int okm_sock_wait_in(struct okm_desc* d, kal_u64 ns) if (!s->buf) return -ENOMEM; } struct kal_endpoint from; - const struct kal_io_result io = + const kal_intptr io = kal_timeout_recv_from(s->dg, s->buf, OKM_DGRAM_MAX, &from, ns); - if (io.e == kal_err_again) return 0; - if (io.e != kal_ok) return -okm_errno(io.e); - s->pend_len = (unsigned long)io.n; + if (io == -kal_err_again) return 0; + if (io < 0) return -okm_errno((int)-io); + s->pend_len = (unsigned long)io; s->pend_from = from; s->pend_msg = 1; return 1; diff --git a/port/src/okm_opt.h b/port/src/okm_opt.h index 485936a..26d7498 100644 --- a/port/src/okm_opt.h +++ b/port/src/okm_opt.h @@ -45,8 +45,9 @@ #define okm_fs_preopen_count kal_fs_preopen_count #define okm_fs_preopen kal_fs_preopen #define okm_fs_open_dir kal_fs_open_dir -#define okm_fs_open_file kal_fs_open_file #define okm_fs_open kal_fs_open +#define okm_fs_props kal_fs_props +#define okm_fs_max_name kal_fs_max_name #define okm_fs_close_dir kal_fs_close_dir #define okm_fs_close_file kal_fs_close_file #define okm_fs_stream kal_fs_stream @@ -61,6 +62,45 @@ #define okm_fs_list_begin kal_fs_list_begin #define okm_fs_list_next kal_fs_list_next +/* ⚠️ WEAK, BECAUSE A VOLUME MAY HAVE NO SUCH NODES AND AN IMPLEMENTATION MAY + * NOT BE ABLE TO MAKE ONE. These are operations of `openkal.fs' and are present + * wherever it is, so the weakness is not about the interface --- it is about a + * backend built before they existed. `kal_fs_props' answers, per directory, + * whether they can be used, and this port asks before it calls. */ +extern __typeof(kal_fs_link_create) kal_fs_link_create __attribute__((__weak__)); +extern __typeof(kal_fs_link_read) kal_fs_link_read __attribute__((__weak__)); + +/* ⚠️⚠️ AND THEY GO THROUGH THE SEAM LIKE EVERYTHING ELSE, WHICH THEY DID NOT. + * + * The two call sites named `kal_fs_link_*' directly and tested the weak symbol + * themselves. That is correct where `openkal.fs' is present and is not a + * declaration at all where it is absent --- the weak declarations above are + * inside this branch, so in the OKM_HAS_FS == 0 configuration the names came + * from STRONG, and a backend with no filesystem failed to link: + * + * ld.lld: error: undefined symbol: kal_fs_link_read + * >>> referenced by okm_syscall.c:340 ... (do_readlink) + * + * ⭐ WHICH IS THE FAILURE THIS FILE'S OWN OPENING COMMENT PREDICTS, IN THE + * WORDS IT PREDICTS IT IN: "a forty-first added later would be the one that was + * missed --- and missed silently, because the way it shows is a link failure on + * a target nobody was building at the time". It was found by openkal-opensbi's + * bare-metal row, which is the only row that builds this configuration. + * + * So the null test lives here, once, and the callers name `okm_fs_link_*'. */ +static inline int okm_fs_link_create(struct kal_dir base, const char* name, + kal_uintptr len, const char* target, + kal_uintptr target_len, kal_uintptr flags) { + if (!kal_fs_link_create) return kal_err_not_supported; + return kal_fs_link_create(base, name, len, target, target_len, flags); +} +static inline kal_intptr okm_fs_link_read(struct kal_dir base, const char* name, + kal_uintptr len, char* out, + kal_uintptr cap) { + if (!kal_fs_link_read) return -kal_err_not_supported; + return kal_fs_link_read(base, name, len, out, cap); +} + #else /* `static inline' and not a macro, so that an argument is still type-checked @@ -68,12 +108,12 @@ * configuration that does not use it. A macro expanding to a constant would * make this file the one place where a signature change goes unnoticed. */ static inline kal_uintptr okm_fs_preopen_count(void) { return 0; } -static inline int okm_fs_preopen(kal_uintptr, struct kal_dir*, const char**, - kal_uintptr*) { return kal_err_not_supported; } +static inline int okm_fs_preopen(kal_uintptr, struct kal_dir*, char*, + kal_uintptr, kal_uintptr*) { return kal_err_not_supported; } +static inline kal_uintptr okm_fs_props(struct kal_dir) { return 0; } +static inline kal_uintptr okm_fs_max_name(void) { return 0; } static inline int okm_fs_open_dir(struct kal_dir, const char*, kal_uintptr, struct kal_dir*) { return kal_err_not_supported; } -static inline int okm_fs_open_file(struct kal_dir, const char*, kal_uintptr, int, - int, struct kal_file*) { return kal_err_not_supported; } static inline int okm_fs_open(struct kal_dir, const char*, kal_uintptr, kal_uintptr, struct kal_file*) { return kal_err_not_supported; } /* Releasing a handle that cannot have been obtained. There is nothing to @@ -84,24 +124,33 @@ static inline void okm_fs_close_file(struct kal_file) {} * handle no operation above can have produced. A caller that reaches here has * already ignored a failure, and zero is what it then passes to * `kal_stream_write', which refuses it. */ -static inline kal_uintptr okm_fs_stream(struct kal_file) { return 0; } +static inline struct kal_stream okm_fs_stream(struct kal_file) { + struct kal_stream s = { 0 }; + return s; +} static inline int okm_fs_seek(struct kal_file, kal_i64, int, kal_u64*) { return kal_err_not_supported; } static inline int okm_fs_truncate(struct kal_file, kal_u64) { return kal_err_not_supported; } static inline int okm_fs_info(struct kal_dir, const char*, kal_uintptr, + kal_uintptr, kal_u32, struct kal_node_info*) { return kal_err_not_supported; } static inline int okm_fs_mkdir(struct kal_dir, const char*, kal_uintptr) { return kal_err_not_supported; } +static inline int okm_fs_link_create(struct kal_dir, const char*, kal_uintptr, + const char*, kal_uintptr, + kal_uintptr) { return kal_err_not_supported; } +static inline kal_intptr okm_fs_link_read(struct kal_dir, const char*, kal_uintptr, + char*, kal_uintptr) { return -kal_err_not_supported; } static inline int okm_fs_remove(struct kal_dir, const char*, kal_uintptr) { return kal_err_not_supported; } static inline int okm_fs_rename(struct kal_dir, const char*, kal_uintptr, struct kal_dir, const char*, kal_uintptr) { return kal_err_not_supported; } -static inline int okm_fs_file_info(struct kal_file, +static inline int okm_fs_file_info(struct kal_file, kal_u32, struct kal_node_info*) { return kal_err_not_supported; } static inline int okm_fs_set_modified(struct kal_file, kal_u64) { return kal_err_not_supported; } static inline int okm_fs_list_begin(struct kal_dir, kal_uintptr*) { return kal_err_not_supported; } -static inline int okm_fs_list_next(struct kal_dir, kal_uintptr*, const char**, +static inline int okm_fs_list_next(struct kal_dir, kal_uintptr*, char*, kal_uintptr, kal_uintptr*, int*) { return kal_err_not_supported; } #endif /* OKM_HAS_FS */ @@ -113,26 +162,25 @@ static inline int okm_fs_list_next(struct kal_dir, kal_uintptr*, const char**, #define okm_process_terminate kal_process_terminate #define okm_process_close kal_process_close -/* ⚠️⚠️ A CAPABILITY WORD IS DATA, AND A WEAK REFERENCE TO DATA IS NOT TESTED THE - * WAY A WEAK REFERENCE TO A FUNCTION IS. - * - * The port holds twenty-five weak references and every one of them names a - * function, so the established form is `if (kal_net_connect) kal_net_connect(…)' - * --- the name decays to its address and the test is upon that address. +/* ⭐⭐ THIS WAS THE ONE WEAK REFERENCE THAT NEEDED A DIFFERENT SPELLING, AND + * openkal 0.9 REMOVED THE REASON. * - * `kal_process_props' is an object. The same spelling would read the object, - * and where the definition is absent the object is at address zero, so the test - * intended to prevent a null dereference IS one. The address must be taken. + * `kal_process_props' was an OBJECT. Every other weak reference in this port + * names a function, so the established form is `if (kal_net_connect) …' --- the + * name decays to its address and the test is upon that address. Written the + * same way for an object, the test READS the object, and where the definition + * is absent the object is at address zero: the test intended to prevent a null + * dereference WAS one. The address had to be taken, and this file existed in + * part to say so. * - * ⭐ This is the defect reported as openkal-linux#13 --- a call through a null - * stub --- arriving through data rather than through code, and it is stated here - * once so that no caller has to remember which kind of symbol it is holding. */ -extern const kal_uintptr kal_process_props __attribute__((__weak__)); + * Version 0.9 makes every report an operation. The two kinds of symbol are one + * kind, the special case is gone, and no caller has to remember which it is + * holding. */ +extern __typeof(kal_process_props) kal_process_props __attribute__((__weak__)); static inline kal_uintptr okm_process_props(void) { - if (&kal_process_props == (const kal_uintptr*)0) return 0; - return kal_process_props; + return kal_process_props ? kal_process_props() : 0; } #else diff --git a/port/src/okm_poll.c b/port/src/okm_poll.c index f17d2b9..8d0eb6c 100644 --- a/port/src/okm_poll.c +++ b/port/src/okm_poll.c @@ -66,10 +66,9 @@ long okm_timed_read(kal_uintptr stream, void* buf, unsigned long len, kal_u64 ns if (!kal_timeout_read) return -ENOSYS; if (len == 0) return 0; struct kal_stream s; s.h = stream; - const struct kal_io_result io = kal_timeout_read(s, buf, len, ns); - if (io.e == kal_err_again) return -EAGAIN; - if (io.e != kal_ok) return -okm_errno(io.e); - return (long)io.n; + const kal_intptr io = kal_timeout_read(s, buf, len, ns); + if (io < 0) return io == -kal_err_again ? -EAGAIN : -okm_errno((int)-io); + return (long)io; } long okm_timed_write(kal_uintptr stream, const void* buf, unsigned long len, kal_u64 ns) @@ -77,10 +76,11 @@ long okm_timed_write(kal_uintptr stream, const void* buf, unsigned long len, kal if (!kal_timeout_write) return -ENOSYS; if (len == 0) return 0; struct kal_stream s; s.h = stream; - const struct kal_io_result io = kal_timeout_write(s, buf, len, ns); - if (io.e == kal_err_again) return io.n ? (long)io.n : -EAGAIN; - if (io.e != kal_ok) return io.n ? (long)io.n : -okm_errno(io.e); - return (long)io.n; + const kal_intptr io = kal_timeout_write(s, buf, len, ns); + /* The count, or the condition when nothing moved --- which is what this + * code used to compute from the pair by hand at every site. */ + if (io < 0) return io == -kal_err_again ? -EAGAIN : -okm_errno((int)-io); + return (long)io; } /* --- the read-ahead ---------------------------------------------------------- */ @@ -124,10 +124,10 @@ static int wait_in(struct okm_desc* d, kal_u64 ns) unsigned char byte = 0; struct kal_stream s; s.h = d->stream; - const struct kal_io_result io = kal_timeout_read(s, &byte, 1, ns); - if (io.e == kal_err_again) return 0; - if (io.e != kal_ok) return -okm_errno(io.e); - if (io.n == 0) { + const kal_intptr io = kal_timeout_read(s, &byte, 1, ns); + if (io == -kal_err_again) return 0; + if (io < 0) return -okm_errno((int)-io); + if (io == 0) { /* End of input. A read will return zero without waiting, which is * exactly what POLLIN asserts, so the descriptor is ready and stays * ready --- the flag is not cleared by the read that observes it. */ diff --git a/port/src/okm_spawn.c b/port/src/okm_spawn.c index e14911d..0051810 100644 --- a/port/src/okm_spawn.c +++ b/port/src/okm_spawn.c @@ -115,9 +115,9 @@ static int place(struct kal_spawn_streams* s, int pos, kal_uintptr stream, if (stream == okm_std_stream(pos)) stream = 0; else if (stream == 0) return ENOSYS; else *placed = 1; - if (pos == 0) s->in = stream; - else if (pos == 1) s->out = stream; - else s->err = stream; + if (pos == 0) s->in.h = stream; + else if (pos == 1) s->out.h = stream; + else s->err.h = stream; return 0; } @@ -202,7 +202,7 @@ int __posix_spawn(pid_t* restrict res, const char* restrict path, /* The streams the started program receives. Zero means it inherits the * corresponding stream of its parent, which is what an environment with no * general mechanism for passing handles can always provide. */ - struct kal_spawn_streams streams = { 0, 0, 0 }; + struct kal_spawn_streams streams = { { 0 }, { 0 }, { 0 } }; int placed = 0; /* a stream of the caller's choosing */ int refused = seed(&streams, &placed); @@ -267,7 +267,7 @@ int __posix_spawn(pid_t* restrict res, const char* restrict path, okm_open_flags(op->oflag), &f); if (oe != kal_ok) { refused = okm_errno(oe); break; } opened[opened_n++] = f; - refused = place(&streams, op->fd, okm_fs_stream(f), &placed); + refused = place(&streams, op->fd, okm_fs_stream(f).h, &placed); break; } case FDOP_CLOSE: diff --git a/port/src/okm_start.c b/port/src/okm_start.c index e5364f3..019b5a3 100644 --- a/port/src/okm_start.c +++ b/port/src/okm_start.c @@ -119,31 +119,49 @@ static char* dup_counted(const char* s, size_t n) * other way. */ void __okm_init_env(void) { + /* ⭐ EACH VALUE IS COPIED, AND THE LENGTH REPORTED IS THE VALUE'S OWN. It + * was answered with a pointer into the implementation's storage, which is + * meaningful only while the implementation shares this address space --- and + * this library is precisely the consumer that must not depend on which way + * it was reached. A capacity of zero asks for the length; asking twice is + * cheaper than a buffer that might be too small. */ const kal_uintptr argc = kal_env_arg_count(); g_argc = 0; for (kal_uintptr i = 0; i < argc && g_argc < 255; i++) { - kal_uintptr len = 0; - const char* a = kal_env_arg(i, &len); - if (!a) break; - char* p = dup_counted(a, len); + const kal_intptr len = kal_env_arg(i, 0, 0); + if (len < 0) break; + char* p = kal_alloc((kal_uintptr)len + 1, 1); if (!p) break; + if (kal_env_arg(i, p, (kal_uintptr)len) != len) break; + p[len] = 0; g_argv_store[g_argc++] = p; } g_argv_store[g_argc] = 0; + /* Enumeration answers a NAME, and the value is then looked up by it. Two + * small operations rather than one that answers both: the one that answered + * both needed two buffers, two capacities and two lengths, and its second + * half was the lookup written again. The set does not change while the + * program runs, so the index holds across the two calls. */ int envc = 0; const kal_uintptr n = kal_env_var_count(); for (kal_uintptr i = 0; i < n && envc < 511; i++) { - kal_uintptr nlen = 0, vlen = 0; - const char* value = 0; - const char* name = kal_env_var_at(i, &nlen, &value, &vlen); + const kal_intptr nlen = kal_env_var_at(i, 0, 0); + if (nlen < 0) break; + char* name = kal_alloc((kal_uintptr)nlen + 1, 1); if (!name) break; - char* p = kal_alloc(nlen + vlen + 2, 1); + if (kal_env_var_at(i, name, (kal_uintptr)nlen) != nlen) break; + name[nlen] = 0; + + const kal_intptr vlen = kal_env_var(name, (kal_uintptr)nlen, 0, 0); + const kal_uintptr have = vlen < 0 ? 0 : (kal_uintptr)vlen; + char* p = kal_alloc((kal_uintptr)nlen + have + 2, 1); if (!p) break; - for (kal_uintptr k = 0; k < nlen; k++) p[k] = name[k]; + for (kal_intptr k = 0; k < nlen; k++) p[k] = name[k]; p[nlen] = '='; - for (kal_uintptr k = 0; k < vlen; k++) p[nlen + 1 + k] = value[k]; - p[nlen + 1 + vlen] = 0; + if (have) kal_env_var(name, (kal_uintptr)nlen, p + nlen + 1, have); + p[nlen + 1 + have] = 0; + kal_free(name, (kal_uintptr)nlen + 1, 1); g_envp_store[envc++] = p; } g_envp_store[envc] = 0; @@ -256,9 +274,50 @@ void __okm_libc_init(void) if (g_libc_up) return; g_libc_up = 1; - libc.page_size = 4096; + /* ⚠️⚠️ THE PAGE WAS FIXED WHEN THIS LIBRARY WAS BUILT AND IS NOW ASKED FOR. + * + * It was the constant 4096 here and again in the auxiliary vector below, + * and it is what this library reports as `sysconf(_SC_PAGESIZE)', what it + * rounds a mapping to, and what it reports as `st_blksize'. On a machine + * whose quantum is sixteen or sixty-four kilobytes --- which is what a + * binary that is distributed rather than built in place meets --- every one + * of those was wrong, and nothing reported it. + * + * openkal 0.9 carries the value because it is a property of the machine the + * program RUNS on and not of the machine it was built for. */ + /* ⚠️⚠️ AND IT IS NOT THE SAME QUANTITY AS THIS LIBRARY'S PAGE SIZE, WHICH + * IS WHAT ASSIGNING IT DIRECTLY ASSUMED. + * + * openkal's granularity is the coarsest quantum a caller must respect. An + * implementation for a machine with no memory management unit answers ONE, + * correctly: there is no page, and nothing needs rounding. + * + * `libc.page_size' is a different thing wearing the same name. This library + * rounds heap growth to it, reports it as `sysconf(_SC_PAGESIZE)' and as + * `st_blksize', and its allocator's arithmetic assumes a power of two no + * smaller than its own quantum. Given one, the allocator asked for + * one-byte extents and the program stopped inside the first allocation + * large enough to need a new one. + * + * ⭐ MEASURED, AND ONLY ON THE MACHINE THAT ANSWERS THAT WAY. Over + * openkal-opensbi the same-source example printed three of its four lines + * and stopped --- containers, exceptions and unwinding all held, and the + * fourth line was the first to format a string. Over openkal-linux, whose + * answer is 4096, nothing was wrong. + * + * So this takes openkal's answer as a FLOOR TO RESPECT rather than as the + * value: at least what the machine requires, at least what this library + * requires, and a power of two because both assume one. */ + kal_uintptr grain = kal_memory_granularity(); + if (grain < 4096) grain = 4096; + else if (grain & (grain - 1)) { /* not a power of two: round up */ + kal_uintptr p = 4096; + while (p && p < grain) p <<= 1; + grain = p ? p : 4096; + } + libc.page_size = grain; fill_random(); - g_auxv[0] = 6 /* AT_PAGESZ */; g_auxv[1] = 4096; + g_auxv[0] = 6 /* AT_PAGESZ */; g_auxv[1] = libc.page_size; g_auxv[2] = 25 /* AT_RANDOM */; g_auxv[3] = (size_t)(uintptr_t)g_random; g_auxv[4] = 0; libc.auxv = g_auxv; diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index bc228c8..27bacbb 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -119,13 +119,12 @@ static syscall_arg_t do_write(int fd, const void* buf, size_t len) struct okm_desc* d = okm_desc_of(fd); if (d->flags & O_NONBLOCK) return okm_timed_write(s, buf, len, OKM_NOW_NS); struct kal_stream st; st.h = s; - const struct kal_io_result io = kal_stream_write(st, buf, len); - /* openkal transfers the whole buffer or reports what prevented it, so a - * short write cannot be reported as success here and is not. Clause 7.4 - * places the loop in the implementation, and this is where the caller's - * copy of that loop would otherwise be. */ - if (io.e != kal_ok) return io.n ? (syscall_arg_t)io.n : -okm_errno(io.e); - return (syscall_arg_t)io.n; + /* ⭐ ONE SIGNED WORD, AND THIS IS THE CODE THAT ARGUED FOR IT. openkal + * returned a count and a condition, and every consumer of that pair --- all + * of them here --- collapsed it by hand and by the same rule: report what + * moved, or the condition when nothing did. Version 0.9 returns that. */ + const kal_intptr n = kal_stream_write(st, buf, len); + return n < 0 ? -okm_errno((int)-n) : (syscall_arg_t)n; } static syscall_arg_t do_read(int fd, void* buf, size_t len) @@ -147,9 +146,8 @@ static syscall_arg_t do_read(int fd, void* buf, size_t len) if (d->flags & O_NONBLOCK) return okm_timed_read(s, buf, len, OKM_NOW_NS); struct kal_stream st; st.h = s; - const struct kal_io_result io = kal_stream_read(st, buf, len); - if (io.e != kal_ok) return -okm_errno(io.e); - return (syscall_arg_t)io.n; + const kal_intptr n = kal_stream_read(st, buf, len); + return n < 0 ? -okm_errno((int)-n) : (syscall_arg_t)n; } /* --- opening --------------------------------------------------------------- */ @@ -162,6 +160,38 @@ static syscall_arg_t do_openat(int dirfd, const char* path, int flags, int mode) if (r) return r; const size_t n = slen(at.rel); + /* ⭐⭐ O_NOFOLLOW ON A LINK IS `ELOOP', AND ANSWERING `ENOENT' MADE A + * DIRECTORY UNREMOVABLE. + * + * openkal states that opening RESOLVES and offers no form that declines + * to --- deliberately, since a program that opens a link to read its + * bytes is asking what `kal_fs_link_read' answers. So this reached the + * link's target, and for a link whose target is absent that is `ENOENT'. + * + * ⚠️ WHICH IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION. POSIX says + * ELOOP: `the name is a link and you said not to follow one'. ENOENT + * says `there is no such name', and the two are acted upon differently + * by exactly the caller that passes this flag. + * + * ⭐ MEASURED THROUGH THREE LAYERS. libc++'s `remove_all' descends by + * opening each entry O_DIRECTORY|O_NOFOLLOW: on ELOOP or ENOTDIR it + * unlinks the entry, on ENOENT it concludes the entry has already gone + * and moves on. Against this port it moved on, unlinked nothing, and + * then `rmdir' failed --- so `fs::remove_all' returned ENOTEMPTY and + * left the tree standing, while every individual operation it is built + * from behaved correctly. The host toolchain removed the same tree. + * + * The enquiry that answers this is the one openkal 0.9 added: ask about + * the name ITSELF rather than what it refers to. It is one call, on a + * path taken only when the caller passed the flag. */ + if (flags & O_NOFOLLOW) { + struct kal_node_info self = { .self_size = sizeof self }; + if (okm_fs_info(at.base, at.rel, n, KAL_FS_NO_RESOLVE, + KAL_INFO_KIND, &self) == kal_ok + && self.kind == kal_node_link) + return -ELOOP; + } + okm_lock(); const int fd = okm_fd_alloc(0); if (fd < 0) { okm_unlock(); return fd; } @@ -197,7 +227,7 @@ static syscall_arg_t do_openat(int dirfd, const char* path, int flags, int mode) } if (e != kal_ok) { okm_unlock(); return -okm_errno(e); } struct kal_dir nod = { 0 }; - okm_fd_bind(fd, OKM_FILE, okm_fs_stream(f), f, nod, flags); + okm_fd_bind(fd, OKM_FILE, okm_fs_stream(f).h, f, nod, flags); okm_unlock(); return fd; } @@ -234,19 +264,42 @@ static void fill_kstat(const struct kal_node_info* in, struct kstat* out) out->st_ctime_sec = out->st_mtime_sec; out->st_ctime_nsec = out->st_mtime_nsec; out->st_uid = 1000; out->st_gid = 1000; - out->st_ino = 0; out->st_dev = 1; + + /* ⚠️⚠️ THE IDENTITY WAS A CONSTANT, SO EVERY FILE WAS THE SAME FILE. + * + * `st_ino' was 0 and `st_dev' was 1 for every node, and nothing reported + * that they were not answers. Measured through the C++ library above: + * `std::filesystem::equivalent("a.txt", "b.txt")' answered TRUE for two + * separately created files, with no error --- the one shape this port + * exists to refuse, arriving through a field nobody had thought of as an + * answer. + * + * openkal 0.9 carries it. An implementation that cannot distinguish nodes + * leaves the position clear, and this reports zero for both --- which is + * what a caller must not read as sameness, and is why `st_dev' is left at + * zero as well rather than at a constant that would make two unknowns + * compare equal. */ + if (in->present & KAL_INFO_IDENTITY) { + out->st_dev = (dev_t)in->identity[0]; + out->st_ino = (ino_t)in->identity[1]; + } else { + out->st_dev = 0; + out->st_ino = 0; + } } static syscall_arg_t do_fstat(int fd, struct kstat* st) { struct okm_desc* d = okm_desc_of(fd); if (!d) return -EBADF; - struct kal_node_info info; + struct kal_node_info info = { .self_size = sizeof info }; if (d->kind == OKM_FILE) { - const int e = okm_fs_file_info(d->file, &info); + const int e = okm_fs_file_info(d->file, KAL_INFO_ALL, &info); if (e != kal_ok) return -okm_errno(e); } else if (d->kind == OKM_DIR) { for (unsigned i = 0; i < sizeof info; i++) ((char*)&info)[i] = 0; + info.self_size = sizeof info; + info.present = KAL_INFO_KIND | KAL_INFO_WRITABLE; info.kind = kal_node_directory; info.writable = 1; } else { @@ -254,6 +307,8 @@ static syscall_arg_t do_fstat(int fd, struct kstat* st) * buffering discipline, and what decides that is whether the stream is * interactive, which openkal reports. */ for (unsigned i = 0; i < sizeof info; i++) ((char*)&info)[i] = 0; + info.self_size = sizeof info; + info.present = KAL_INFO_KIND | KAL_INFO_WRITABLE; info.kind = kal_node_other; info.writable = 1; } @@ -272,14 +327,69 @@ static syscall_arg_t do_fstat(int fd, struct kstat* st) return 0; } +/* Reads the content of a node that names another. + * + * POSIX truncates into the caller's buffer and reports what it wrote; openkal + * reports the length the content HAS. The two are reconciled here, which is + * where the difference belongs: a caller of `readlink' expects the first. */ +static syscall_arg_t do_readlink(int dirfd, const char* path, char* out, size_t cap) +{ + struct okm_at at; + const syscall_arg_t r = okm_resolve(dirfd, path, &at, 0); + if (r) return r; + + /* A name that is not such a node is EINVAL, and one that is absent is + * ENOENT --- two answers `kal_fs_link_read' does not distinguish for us, + * so the enquiry is made first. It declines to resolve, because the + * question is about the node itself. */ + struct kal_node_info info = { .self_size = sizeof info }; + const int ie = okm_fs_info(at.base, at.rel, slen(at.rel), + KAL_FS_NO_RESOLVE, KAL_INFO_KIND, &info); + if (ie != kal_ok) return -okm_errno(ie); + if (info.kind == kal_node_absent) return -ENOENT; + if (info.kind != kal_node_link) return -EINVAL; + + const kal_intptr n = okm_fs_link_read(at.base, at.rel, slen(at.rel), out, cap); + if (n < 0) return -okm_errno((int)-n); + return (syscall_arg_t)((size_t)n < cap ? (size_t)n : cap); +} + +/* Makes a node whose content is another name. + * + * ⚠️ THE CONTENT IS NOT A NAME THIS INTERFACE RESOLVES. It is stored and read + * later by whoever follows it, so it is not put through the resolution that + * would refuse one that ascends --- and one that ascends is the ordinary case + * for a relative target. Only the name being CREATED is resolved. */ +static syscall_arg_t do_symlink(const char* target, int dirfd, const char* path) +{ + if (!target || !path) return -EFAULT; + struct okm_at at; + const syscall_arg_t r = okm_resolve(dirfd, path, &at, 0); + if (r) return r; + const int e = okm_fs_link_create(at.base, at.rel, slen(at.rel), + target, slen(target), 0); + return e == kal_ok ? 0 : -okm_errno(e); +} + static syscall_arg_t do_fstatat(int dirfd, const char* path, struct kstat* st, int flag) { if ((flag & AT_EMPTY_PATH) && path && !*path) return do_fstat(dirfd, st); struct okm_at at; const syscall_arg_t r = okm_resolve(dirfd, path, &at, 0); if (r) return r; - struct kal_node_info info; - const int e = okm_fs_info(at.base, at.rel, slen(at.rel), &info); + /* ⭐⭐ `stat' AND `lstat' ARE TWO QUESTIONS AND THIS ANSWERED ONE OF THEM + * TWICE. The flag was ignored, so both asked about the name itself while + * `open' resolved --- a program was told a name referred to a link when + * opening it would have reached a file. Through the C++ library above: + * `is_regular_file' false for a name whose bytes it could read, + * `file_size' refused, `exists' TRUE for a name that finally referred to + * nothing, and one such node made a whole directory tree uncopyable. + * + * openkal 0.9 states which operations resolve, beside each of them, and + * `kal_fs_info' takes the choice. */ + struct kal_node_info info = { .self_size = sizeof info }; + const kal_uintptr how = (flag & AT_SYMLINK_NOFOLLOW) ? KAL_FS_NO_RESOLVE : 0; + const int e = okm_fs_info(at.base, at.rel, slen(at.rel), how, KAL_INFO_ALL, &info); if (e != kal_ok) return -okm_errno(e); if (info.kind == kal_node_absent) return -ENOENT; fill_kstat(&info, st); @@ -318,11 +428,15 @@ static syscall_arg_t do_getdents(int fd, void* buf, size_t cap) name = d->pending_name; kind = d->pending_kind; len = slen(d->pending_name); } else { - const int e = okm_fs_list_next(d->dir, &d->iter, &name, &len, &kind); + /* The name is copied into this layer's own buffer, which is what + * the operation now does directly --- it reports the length the + * name HAS, so one longer than this buffer is skipped rather than + * silently truncated. */ + const int e = okm_fs_list_next(d->dir, &d->iter, held, sizeof held - 1, + &len, &kind); if (e != kal_ok) return used ? (syscall_arg_t)used : -okm_errno(e); - if (!name) { d->iter = 0; break; } /* the iterator is spent */ + if (d->iter == 0) break; /* the iterator is spent */ if (len >= sizeof held) continue; /* a name this layer cannot carry */ - for (kal_uintptr i = 0; i < len; i++) held[i] = name[i]; held[len] = 0; name = held; } @@ -391,7 +505,26 @@ static void to_timespec(kal_duration ns, struct timespec* ts) /* --- processes -------------------------------------------------------------- */ -#define OKM_MAX_CHILD 64 +/* ⚠️ THE BOUND IS PART OF THE CONTRACT, AND IT WAS NOT. + * + * An entry is taken when a program is started and released when it is waited + * for, which is what a process table is; a program that starts programs and + * never waits for them holds entries for ever, and the next start reports + * EAGAIN. That is what POSIX says `fork' does when the table is full, so the + * behaviour is right --- what was wrong is that the number was invisible. + * + * ⭐ Measured: the sixty-fifth `posix_spawn' failed with `Resource temporarily + * unavailable' on a program that had started sixty-four and waited for none, + * and on one that polled each with WNOHANG once and did not come back --- and a + * caller meeting that has an error on an operation with no evident relation to + * the ones that caused it. The README now states this bound beside the + * descriptor and open-description bounds it already stated, and `sysconf' is + * not the place because POSIX has no enquiry that answers it. + * + * The number is raised because sixty-four is small for a program that manages + * others --- which is the kind of program that meets it --- and because each + * entry is three words. */ +#define OKM_MAX_CHILD 256 static struct { int used; int pid; struct kal_process h; } g_child[OKM_MAX_CHILD]; static int g_next_pid = 1000; @@ -606,12 +739,14 @@ static int trace_wanted(void) static const char name[] = "OPENKAL_MUSL_TRACE"; static const char wanted[] = "enosys"; - kal_uintptr len = 0; - const char* v = kal_env_var(name, sizeof name - 1, &len); + /* Copied into a buffer of this file's own. Nothing here allocates --- what + * failed may be the operation that the allocator was about to perform. */ + char v[16]; + const kal_intptr n = kal_env_var(name, sizeof name - 1, v, sizeof v); want = 0; - if (v && len == sizeof wanted - 1) { + if (n == (kal_intptr)(sizeof wanted - 1)) { want = 1; - for (kal_uintptr i = 0; i < len; i++) + for (kal_intptr i = 0; i < n; i++) if (v[i] != wanted[i]) { want = 0; break; } } __atomic_store_n(&g_want, want, __ATOMIC_RELEASE); @@ -1097,8 +1232,9 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, struct okm_at at; const syscall_arg_t r = okm_resolve((int)a1, (const char*)a2, &at, 0); if (r) return r; - struct kal_node_info info; - const int e = okm_fs_info(at.base, at.rel, slen(at.rel), &info); + struct kal_node_info info = { .self_size = sizeof info }; + const int e = okm_fs_info(at.base, at.rel, slen(at.rel), 0, + KAL_INFO_KIND | KAL_INFO_WRITABLE, &info); if (e != kal_ok) return -okm_errno(e); if (info.kind == kal_node_absent) return -ENOENT; if (((int)a3 & W_OK) && !info.writable) return -EACCES; @@ -1109,8 +1245,9 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, struct okm_at at; const syscall_arg_t r = okm_resolve(AT_FDCWD, (const char*)a1, &at, 0); if (r) return r; - struct kal_node_info info; - const int e = okm_fs_info(at.base, at.rel, slen(at.rel), &info); + struct kal_node_info info = { .self_size = sizeof info }; + const int e = okm_fs_info(at.base, at.rel, slen(at.rel), 0, + KAL_INFO_KIND | KAL_INFO_WRITABLE, &info); if (e != kal_ok) return -okm_errno(e); if (info.kind == kal_node_absent) return -ENOENT; if (((int)a2 & W_OK) && !info.writable) return -EACCES; @@ -1128,32 +1265,22 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_chdir: return okm_chdir(AT_FDCWD, (const char*)a1); case SYS_fchdir: return okm_chdir((int)a1, 0); - case SYS_readlinkat: { - struct okm_at at; - const syscall_arg_t r = okm_resolve((int)a1, (const char*)a2, &at, 0); - if (r) return r; - struct kal_node_info info; - const int e = okm_fs_info(at.base, at.rel, slen(at.rel), &info); - if (e != kal_ok) return -okm_errno(e); - if (info.kind == kal_node_absent) return -ENOENT; - /* A name that is not a symbolic link is answered as POSIX answers it. - * A name that is one cannot be read: openkal reserves the operations - * upon links to an interface it has not defined, so the honest answer - * is that the operation is unavailable rather than that the link - * points at nothing. */ - return info.kind == kal_node_link ? -ENOSYS : -EINVAL; - } + /* ⭐ NODES WHOSE CONTENT IS ANOTHER NAME. openkal 0.9 carries the two + * operations, so these answer rather than refusing. + * + * ⚠️ THE ENQUIRY IS ASKED FIRST AND IT TAKES THE DIRECTORY, which is the + * whole reason these can be operations of `openkal.fs' at all: the same + * implementation succeeds on one volume and fails on another, so a caller + * that could not ask would be left to discover it by the attempt. */ + case SYS_readlinkat: return do_readlink((int)a1, (const char*)a2, + (char*)a3, (size_t)a4); #ifdef SYS_readlink - case SYS_readlink: { - struct okm_at at; - const syscall_arg_t r = okm_resolve(AT_FDCWD, (const char*)a1, &at, 0); - if (r) return r; - struct kal_node_info info; - const int e = okm_fs_info(at.base, at.rel, slen(at.rel), &info); - if (e != kal_ok) return -okm_errno(e); - if (info.kind == kal_node_absent) return -ENOENT; - return info.kind == kal_node_link ? -ENOSYS : -EINVAL; - } + case SYS_readlink: return do_readlink(AT_FDCWD, (const char*)a1, + (char*)a2, (size_t)a3); +#endif + case SYS_symlinkat: return do_symlink((const char*)a1, (int)a2, (const char*)a3); +#ifdef SYS_symlink + case SYS_symlink: return do_symlink((const char*)a1, AT_FDCWD, (const char*)a2); #endif /* --- descriptors ------------------------------------------------------ */ @@ -1656,6 +1783,48 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * which takes a different path when privileged takes the ordinary one. */ case SYS_getuid: case SYS_geteuid: case SYS_getgid: case SYS_getegid: return 1000; + + /* ⚠️ `getpgrp' HANDED A NEGATED ERROR TO ITS CALLER AS A PROCESS GROUP. + * + * There was no case for this number, so the default arm answered -ENOSYS + * --- and musl's `getpgrp' is `return __syscall(SYS_getpgid, 0);' WITHOUT + * `__syscall_ret', deliberately, because POSIX says the call cannot fail. + * So a program that asked was told its group was -38: not -1, no errno, + * and nothing to check. Measured. + * + * openkal has no process groups and no sessions --- `kill' resolves + * children only --- so the honest answer for the calling program is the + * identity `getpid' already reports, which is the whole truth here: there + * is one program and it is in its own group. A group that is not this + * program's is a group this environment has no way to name, and is + * refused. `setpgid' and `setsid' remain refused: making a group is not + * the same as being in one, and reporting success for it would be + * reporting an effect that does not exist. */ + case SYS_getpgid: + return (a1 == 0 || a1 == 1) ? 1 : -ESRCH; +#ifdef SYS_getpgrp + case SYS_getpgrp: return 1; +#endif + case SYS_getsid: + return (a1 == 0 || a1 == 1) ? 1 : -ESRCH; + + /* ⭐ REFUSED FROM A CASE OF ITS OWN RATHER THAN FROM THE DEFAULT ARM, SO + * THAT THE TRACE DOES NOT REPORT IT. + * + * musl's `pthread_create' calls `__membarrier_init' the first time a + * context is started, and that function is one line whose RESULT IS + * ASSIGNED TO NOTHING --- musl's own comment says the registration is an + * optimisation and failing it costs nothing. Nothing in this port's musl + * ever calls `__membarrier' itself. + * + * ⚠️ It reached the default arm, so `OPENKAL_MUSL_TRACE=enosys' reported it + * beside five operations that a program actually wanted, and the first + * consumer to use that switch had to work out which of the six mattered. + * A trace whose reader must filter it is a trace that costs its reader + * more than it saves. */ +#ifdef SYS_membarrier + case SYS_membarrier: return -ENOSYS; +#endif /* SYS_umask is deliberately absent and falls to the default below. * * IT USED TO BE ANSWERED, AND THE ANSWER WAS A FICTION. A stored word was @@ -1693,15 +1862,61 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * accepted and can never run is exactly the silent wrongness clause 3.1 * names, so the request is refused and the program learns it. */ case SYS_rt_sigprocmask: { - sigset_t* old = (sigset_t*)a3; - if (old) for (unsigned i = 0; i < sizeof *old; i++) ((char*)old)[i] = 0; + /* ⚠️⚠️ THE SIZE IS THE CALLER'S, AND TAKING IT FROM THE TYPE INSTEAD + * DESTROYED THE CALLER'S RETURN ADDRESS. + * + * This wrote `sizeof(sigset_t)' --- 128 bytes --- into whatever `a3' + * named. `a4' is the SIGSETSIZE the caller declared, which is the + * kernel's set and is EIGHT on this architecture; the kernel writes + * exactly that many and no more. + * + * Sixteen of musl's seventeen callers pass a 128-byte `sigset_t' and + * saw nothing. The seventeenth is `src/signal/sigaction.c', which + * declares `unsigned long set[_NSIG/(8*sizeof(long))]' --- ONE WORD --- + * and reaches this only when the signal is SIGABRT. That local sits at + * -0x20 in a frame of 0x30, so 120 bytes past it lay the saved frame + * pointer and the return address, and `__sigaction' returned to zero. + * + * ⭐ MEASURED, and the whole of the reproduction is three lines: + * + * int main(void) { signal(SIGABRT, h); return 0; } + * + * SIGSEGV with the instruction pointer at zero, the stack top zero + * rather than a return address, and every register but one zero --- + * which is a RETURN to a cleared return address and not a call through + * a null pointer, and reads as the second. Every other signal number + * returns SIG_ERR and exits 0. + * + * ⚠️ AND IT WAS NOT ONLY INSTALLING A HANDLER. `signal(SIGABRT, SIG_IGN)' + * and a plain enquiry, `sigaction(SIGABRT, NULL, &old)', died the same + * way: musl takes the lock for any change to that disposition, and + * blocking signals around it is how it takes it. So a program that only + * READ what SIGABRT was set to could not survive doing so --- which a + * terminal-interface library, a test framework's death tests and any + * crash reporter all do before they do anything else. */ + const kal_uintptr room = (kal_uintptr)a4; + if (room > sizeof(sigset_t)) return -EINVAL; + char* old = (char*)a3; + if (old) for (kal_uintptr i = 0; i < room; i++) old[i] = 0; return 0; } case SYS_rt_sigaction: { + /* ⚠️ THE SAME DEFECT'S OTHER HALF, IN THE OTHER DIRECTION. The + * old-action was cleared for the size of a structure declared HERE --- + * three fields --- while `struct k_sigaction' is four, so eight bytes + * of the caller's structure were left holding whatever the stack held + * and were then copied out to the program by `__libc_sigaction'. The + * size is taken from the caller's declared sigsetsize, as the kernel + * takes it, and the layout from the architecture the port is built + * for. */ const struct { void* handler; unsigned long flags; void* restorer; }* act = (const void*)a2; if (a3) { + /* handler, flags, restorer, and the mask whose width the caller + * declared in a4. */ + const kal_uintptr mask = (kal_uintptr)a4; + if (mask > sizeof(sigset_t)) return -EINVAL; char* old = (char*)a3; - for (unsigned i = 0; i < sizeof *act; i++) old[i] = 0; + for (kal_uintptr i = 0; i < sizeof *act + mask; i++) old[i] = 0; } if (!act) return 0; const uintptr_t h = (uintptr_t)act->handler;