diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d89e6e..232c497 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,20 @@ jobs: fail-fast: false matrix: include: - - { name: 'linux, gcc', os: ubuntu-24.04, toolchain: 'gcc@16.1.0', target: '' } - - { name: 'linux, llvm', os: ubuntu-24.04, toolchain: 'llvm@22.1.8', target: '' } - - { name: 'macos, llvm', os: macos-14, toolchain: 'llvm@20.1.7', target: '' } - - { name: 'windows, gcc', os: windows-2022, toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu' } + # ⭐ `net`, `fork` and `shell` STATE WHAT THE BACKEND BENEATH THIS ROW + # PROVIDES, and they are part of the criterion rather than a + # convenience. openkal permits an implementation to decline an + # interface in whole (clause 3), so "the probe was not run" and "the + # probe was run and the refusal was the expected answer" are + # different outcomes, and only the second is evidence. + - { name: 'linux, gcc', os: ubuntu-24.04, toolchain: 'gcc@16.1.0', target: '', net: 'yes', fork: '--fork', shell: '--shell' } + - { name: 'linux, llvm', os: ubuntu-24.04, toolchain: 'llvm@22.1.8', target: '', net: 'yes', fork: '--fork', shell: '--shell' } + - { name: 'macos, llvm', os: macos-14, toolchain: 'llvm@20.1.7', target: '', net: 'yes', fork: '--fork', shell: '--shell' } + # ⚠️ openkal-windows declines `openkal.space`: this system has no + # primitive that copies an address space and starts a context in the + # copy, and inventing one would be the simulation clause 3.1 + # forbids. `--no-fork` asserts the refusal. + - { name: 'windows, gcc', os: windows-2022, toolchain: 'gcc@16.1.0', target: 'x86_64-windows-gnu', net: 'yes', fork: '--no-fork', shell: '--no-shell' } defaults: run: shell: bash @@ -199,10 +209,32 @@ jobs: - name: An optional interface is referenced weakly if: runner.os == 'Linux' run: | - obj="$(find target -name okm_syscall.o | head -1)" - test -n "$obj" || { echo "::error::okm_syscall.o was not built"; exit 1; } + set -euo pipefail + # ⚠️ ONE FINGERPRINT DIRECTORY, ASSERTED BEFORE ANYTHING IS READ. + # `target/` accumulates one per configuration, and a search across all + # of them answers for a build that is not this one. The step that + # checks the withheld set already learned this; the assertion is made + # here for the same reason and not by reference to that one. + fps=$(ls -d target/*/*/ 2>/dev/null | wc -l) + [ "$fps" = 1 ] || { echo "::error::expected one fingerprint directory under target/, found $fps" + ls -d target/*/*/ 2>/dev/null | sed 's/^/ /' + exit 1; } + + # ⚠️⚠️ AND THIS PACKAGE'S OWN OBJECTS, WHICH IS NOT WHAT A SEARCH UNDER + # `target/` FINDS. Measured while this step was extended: a search of + # everything under `target/` reported `kal_net_accept` and + # `kal_datagram_recv_from` as undefined STRONG references and the other + # nine as weak — because the DEPENDENCY's objects are there too, under + # `obj/mcpplibs_*`, and openkal-linux's `kal_timeout_accept` refers to + # its own `kal_net_accept` strongly. That is correct for an + # implementation and says nothing about this port. + objs=$(find target/*/*/obj -maxdepth 1 -name '*.o') + n=$(printf '%s\n' "$objs" | grep -c . || true) + echo " examining $n of this package's own objects" + [ "$n" -gt 100 ] || { echo "::error::only $n objects; nothing was examined"; exit 1; } + nm="$(command -v llvm-nm || command -v nm)" - "$nm" "$obj" > syms.txt + "$nm" $objs > syms.txt 2>/dev/null || true # The control: a required interface must still be a strong reference, # so that a backend failing to provide one is still a link error. @@ -213,7 +245,39 @@ jobs: grep -qE '^ *w kal_random_fill$' syms.txt \ || { echo "::error::kal_random_fill is not a weak reference; an optional interface has been made mandatory" grep kal_random syms.txt; exit 1; } - echo " ok kal_random_fill is weak, kal_time_sleep is strong" + + # ⭐⭐ AND THE SAME FOR EVERY INTERFACE THE 0.8 ROUTES REACH, WHICH IS + # WHY THIS STEP IS NOT A SINGLE ASSERTION ANY MORE. + # + # `openkal.net`, `openkal.datagram`, `openkal.timeout` and + # `openkal.space` are optional in exactly the sense `openkal.random` + # is, and openkal-opensbi and openkal-uefi decline all four. A strong + # reference to any one of them would make a bare-metal program that + # never opens a socket fail to link — which is the defect this step + # already exists because of, arriving through a different name. + # + # ⚠️ THE SYMBOL IS SOUGHT ACROSS THE PORT'S OBJECTS RATHER THAN IN ONE, + # because the routes are spread over three sources: okm_net.c, + # okm_poll.c and okm_fork.c. Naming one file would assert a property of + # that file and report nothing about the others. + + weakfail=0 + for name in kal_net_connect kal_net_listen kal_net_accept kal_net_close \ + kal_datagram_open kal_datagram_recv_from \ + kal_timeout_read kal_timeout_write kal_timeout_accept \ + kal_space_start kal_process_channel; do + if grep -qE "^ *U $name\$" syms.txt; then + echo "::error::$name is an undefined STRONG reference; an interface a backend may decline has been made mandatory" + weakfail=1 + elif grep -qE "^ *w $name\$" syms.txt; then + echo " weak: $name" + else + echo "::error::$name is referenced nowhere; the route that was added does not call it" + weakfail=1 + fi + done + [ "$weakfail" = 0 ] || exit 1 + echo " ok every optional interface is referenced weakly, and kal_time_sleep is strong" # ⭐⭐ ASKING WHETHER A STREAM IS A TERMINAL GETS THE RIGHT ANSWER. # @@ -395,66 +459,58 @@ jobs: # A program above this package names one package. It does not name # openkal, it does not name an implementation, and it says nothing about # the platform. + # + # ⚠️ FOUR PROBES, ONE RUNNER. tools/run-probe.sh holds the watchdog, the + # report of where a program that stopped was, and the two readings of the + # output. Four copies of that would be four places for one of them to fall + # behind. - name: The posix probe - working-directory: examples/posix + env: + MCPP_TARGET: ${{ matrix.target }} + run: bash tools/run-probe.sh examples/posix posix + + # ⭐ THE NAMES THIS LIBRARY MUST NOT TAKE FROM A PROGRAM ABOVE IT. + # + # Reported as openkal-musl#13. This one is a COMPILE-TIME criterion: the + # source declares `hidden`, `weak` and `weak_alias` as ordinary + # identifiers, and if any of the three is a macro again the file does not + # compile. It is run as well as built so that something links afterwards, + # which is what distinguishes a source that compiles from a package that + # works. + - name: A program may use the names the internal overlay defines + env: + MCPP_TARGET: ${{ matrix.target }} run: | - extra='' - [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' - mcpp build $extra - binary="$(find target -type f \( -name 'posix' -o -name 'posix.exe' \) | head -1)" - - # A watchdog, because a program that does not return is as much a - # failure as one that returns wrongly, and the job would otherwise - # spend its whole timeout finding that out. Written out rather than - # taken from `timeout', which two of the three systems have and one - # does not. - watch() { # watch ... - local seconds="$1"; shift - "$@" & local pid=$! - ( sleep "$seconds"; kill -9 "$pid" 2> /dev/null ) & local guard=$! - wait "$pid"; local status=$? - kill "$guard" 2> /dev/null || true - return $status - } + bash tools/run-probe.sh examples/identifiers identifiers + grep -q 'hidden+weak+weak_alias = 41' examples/identifiers/run.log - if watch 120 sh -c "\"$binary\" > run.log 2>&1"; then - cat run.log - else - status=$? - echo "--- what the program printed before it stopped (status $status) ---" - cat run.log - - # A program that stopped and a program that did not return need - # different questions asked of them, and "exit code 139" and "the - # job timed out" answer neither. The debugger is for the first; a - # stack sample of a program that is still running is for the second, - # and a debugger asked to run a program that hangs hangs with it. - if [ "$status" -eq 137 ]; then - echo "--- it did not return; where it was ---" - "$binary" > /dev/null 2>&1 & hung=$! - sleep 5 - if command -v sample > /dev/null 2>&1; then - sample "$hung" 3 -mayDie 2>&1 | head -80 || true - elif command -v eu-stack > /dev/null 2>&1; then - eu-stack -p "$hung" 2>&1 | head -60 || true - fi - kill -9 "$hung" 2> /dev/null || true - elif command -v lldb > /dev/null 2>&1; then - watch 90 lldb --batch -o run \ - -k 'thread backtrace all' -k 'register read' -k quit \ - -- "$binary" > crash.log 2>&1 || true - cat crash.log - elif command -v gdb > /dev/null 2>&1; then - watch 90 gdb -batch -ex run -ex 'bt' --args "$binary" > crash.log 2>&1 || true - cat crash.log - fi - exit 1 - fi - # Both directions: that the program reported, and that nothing it - # observed failed to hold. Asserting only the first would pass for a - # program that printed its failures. - grep -qE '^-- failures: 0 --$' run.log - ! grep -q '^FAIL:' run.log + # ⭐⭐ THE ROUTES openkal 0.8 MADE POSSIBLE, EXERCISED THROUGH POSIX. + # + # `socket`, `bind`, `listen`, `accept`, `connect`, `sendto`, `recvfrom`, + # `poll` and `select` reached this port's default arm and returned ENOSYS + # until `openkal.net`, `openkal.datagram` and `openkal.timeout` existed to + # route them onto (openkal-linux#13). The probe names no openkal symbol: + # a probe that called `kal_net_connect` to check that `connect` works + # would be checking the wrong thing. + # + # ⚠️ `matrix.net` DECIDES WHETHER THE ROW RUNS IT, and the value is a + # property of the BACKEND rather than of the system. A backend that + # declines `openkal.net` is behaving correctly; a row that expected it and + # silently did not get it is what this must not read as a pass. + - name: The network probe + if: matrix.net == 'yes' + env: + MCPP_TARGET: ${{ matrix.target }} + run: bash tools/run-probe.sh examples/net net + + # Another program, started three ways. ⭐ WHAT IS EXPECTED IS PASSED IN + # RATHER THAN INFERRED: `--no-fork` requires that duplicating the calling + # image be REFUSED, so a system whose backend declines `openkal.space` is + # asserted to decline it rather than merely not asked. + - name: The subprocess probe + env: + MCPP_TARGET: ${{ matrix.target }} + run: bash tools/run-probe.sh examples/subprocess subprocess ${{ matrix.fork }} ${{ matrix.shell }} - name: The same program, built the ordinary way, as a control run: | diff --git a/.gitignore b/.gitignore index d2deb4a..dcd12b6 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ Thumbs.db # What the examples write while they run. sample.txt *.tmp + +# What tools/run-probe.sh writes beside an example it is running. +run.log +out.log +crash.log diff --git a/README.md b/README.md index 55695ce..6b7013d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the claim can be checked rather than repeated. ```toml [dependencies] -openkal-musl = "0.3.1" +openkal-musl = "0.5.0" ``` It names no implementation and no platform: a C library is the one consumer that @@ -83,6 +83,21 @@ appending, directories with enumeration, renaming, both clocks, execution contexts with contended mutexes, allocation and reallocation, and starting another program and awaiting it. +Sockets, datagrams, readiness and the duplication of the calling image were in +the table below until openkal 0.8, and they are not there now. What changed is +the specification: `openkal.net`, `openkal.datagram`, `openkal.timeout` and +`openkal.space` gave this port the atoms it had been missing, and each of the +four is now composed above them rather than refused. `examples/net` and +`examples/subprocess` assert them, written against POSIX and naming no openkal +symbol. + +**⚠️ Every one of those four depends on what is beneath.** Clause 3 permits an +implementation to provide an interface in whole or not at all, and the four +interfaces are optional. Where a backend declines one, the routes that use it +report `ENOSYS` — the port takes a weak reference and tests it before calling, +so a program that never opens a socket links and runs above a backend that has +no network at all. + The following are absent, and each is refused rather than quietly accepted, because a facility that reports success and does nothing is the one kind of answer that leaves a program wrong without telling it. @@ -91,12 +106,19 @@ answer that leaves a program wrong without telling it. | --- | --- | --- | | signal handlers | `sigaction` reports `ENOSYS` for any handler other than the default or ignore | openkal has no asynchronous delivery. A handler that was accepted and could never run would be silently wrong; masking, which has nothing to mask, succeeds. | | 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. | -| `fork` | `ENOSYS` | openkal has no operation that duplicates the calling image, deliberately: clause 7.1. `posix_spawn` works, and it is what portable programs use. | -| pipes | `ENOSYS` | openkal has no operation that creates one. | -| readiness | `poll`, `select` report `ENOSYS` | `openkal.event` is reserved and unspecified. | -| symbolic links | `readlink` reports `EINVAL` for a name that is not one and `ENOSYS` for one that is | openkal reserves the operations upon links to an interface it has not defined. | -| ownership and permission bits | `chmod` and `chown` report `ENOSYS`; `stat` reports a mode assembled from what openkal knows | openkal reports what a name refers to and whether it may be written, which is what a capability-based environment can report. | -| entropy | `getrandom` reports `ENOSYS` | openkal has no source of one, 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. | +| 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. | +| 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. | + +**⭐ The permission row is a decision and not an omission.** The alternative was +to ask the specification for a permission operation. It was declined: a FAT +volume, a UEFI system partition and a Windows access-control list do not share a +model, so an operation upon permissions is one that some resources of the +interface can never satisfy — which is what clause 6.4 excludes. Refusing here +and stating why is the answer; `.agents/docs` in the specification's repository +records the reasoning. Two further boundaries are properties of the arrangement rather than omissions. @@ -127,17 +149,20 @@ bridge a difference in shape: | | lines | | --- | --- | -| the system-call correspondence (`okm_syscall.c`) | 923 | -| descriptors and name resolution (`okm_fd.c`) | 434 | -| startup and the thread pointer (`okm_start.c`) | 241 | -| `setjmp` and its relatives (`okm_setjmp.S`) | 223 | -| execution contexts and the suspension primitive (`okm_thread.c`) | 202 | -| starting another program (`okm_spawn.c`) | 163 | -| where per-context state is kept (`okm_context.c`) | 128 | +| the system-call correspondence (`okm_syscall.c`) | 1480 | +| sockets and datagrams (`okm_net.c`) | 788 | +| descriptors and name resolution (`okm_fd.c`) | 463 | +| `setjmp` and its relatives (`okm_setjmp.S`) | 316 | +| how a second name is made (`port/include/features.h`) | 288 | +| startup and the thread pointer (`okm_start.c`) | 282 | +| readiness and bounded transfer (`okm_poll.c`) | 256 | +| execution contexts and the suspension primitive (`okm_thread.c`) | 203 | +| starting another program (`okm_spawn.c`) | 164 | +| where per-context state is kept (`okm_context.c`) | 162 | +| duplicating the calling image (`okm_fork.c`) | 109 | | what two object formats do not provide (`okm_format.c`) | 105 | -| how a second name is made (`port/include/features.h`) | 133 | -| mapping, the working directory, the two architecture seams | 241 | -| **total** | **2793** | +| mapping, the working directory, program headers, the architecture seams, the one file each of two object formats needs | 398 | +| **total** | **5014** | Against 1345 musl sources compiled unmodified. The ratio is the measurement: if openkal's decomposition were wrong, the port layer would be where the difference @@ -147,6 +172,19 @@ every one of those is about an object format or a naming convention rather than about a kernel — which is the shape of the result rather than a qualification of it. +**⚠️ It grew by 2221 more when the socket, datagram, readiness and image-copying +routes were added, and that number deserves a reading rather than a footnote.** +Roughly half of it is comment; of the code, the largest single piece is the +state machine in `okm_net.c`, and what that machine bridges is one difference in +shape: BSD makes a socket first and decides what it is afterwards, and openkal +has no object between "nothing" and "a connection". The rest is the read-ahead +that answers a readiness enquiry, which exists because openkal deliberately has +no operation reporting whether a transfer would proceed (clause 6.3). + +Both are shape and neither is environment. They are written once here and are +what every implementation of openkal is spared — which is the property the ratio +was measuring in the first place, and the growth does not change its direction. + ## Verification `examples/wordcount` is an ordinary POSIX program whose source mentions nothing @@ -160,6 +198,26 @@ rather than by a return value: a truncation that did not happen leaves a longer file, an exclusion that did not happen succeeds, and an append that did not happen overwrites. +`examples/net` makes 35 over a listener, a connection and a pair of datagram +endpoints, all on the loopback address, and asks nothing of the network beyond +the machine it runs on. + +`examples/subprocess` starts another program three ways — `fork`, `system`, +`popen`. + +**⭐ Those two say what they expect on the command line rather than inferring +it.** `--fork` requires that duplicating the calling image work; `--no-fork` +requires that it be *refused*. An environment whose backend declines +`openkal.space` is not a failure, and an environment expected to provide it that +quietly does not IS one — a probe that accepted either answer could not tell +them apart, and the interesting failure is exactly the one it could not see. + +`examples/identifiers` compiles rather than runs. It declares `hidden`, `weak` +and `weak_alias` as ordinary identifiers, which a program above this package +could not do until the internal overlay's macros were scoped to the overlay +(`mcpplibs/openkal-musl#13`). If any of the three is a macro again, the file does +not compile. + ## Three object formats, and what each cost | | | @@ -172,11 +230,20 @@ happen overwrites. Each is recorded in `musl/PATCHES.md` rather than left to be discovered. -**`fork` is absent and stays absent.** Duplicating a running image is not -something every environment can produce, and clause 3.1 of the specification -declines to simulate what cannot be supplied. `posix_spawn` — and therefore -`system` and `popen` — are supplied, because musl builds them on starting a -program. +**`fork` is composed here rather than required beneath.** `openkal.space` starts +a context in a *copy of the calling address space*, and stops there: the started +context begins at a function the caller names, not at the instruction the caller +was executing, because that is what can be stated in a C application binary +interface at all. `fork` returns twice, so the second half is this port's: +`setjmp` before the call, `longjmp` in the copy. The specification's own +`space.h` describes that composition and says in terms that it belongs above the +line, which is where it now is (`port/src/okm_fork.c`). + +⚠️ **An earlier version of this file said `fork` was absent and would stay +absent**, on the reading that clause 7.1 declines to duplicate an address space +*and its execution state*. Half of that is right: the clause declines the +**pair**. `openkal.space` supplies the first half by itself, and what was +missing was never an atom. **`execve` is starting a program, waiting for it, and ending with its status.** A caller cannot distinguish that through this library: the same program runs, with @@ -196,6 +263,9 @@ file. | | | | --- | --- | | `examples/posix` | 32 observations, each written so that it can fail | +| `examples/net` | 35 over sockets, datagrams and readiness, on the loopback address | +| `examples/subprocess` | another program started three ways, and a refusal checked as one | +| `examples/identifiers` | three names a program above this library may use, asserted by compiling | | `examples/wordcount` | the same three counts as the system's own `wc` | | [`mcpplibs/sbase`](https://github.com/mcpplibs/sbase) | all 97 suckless base utilities, sources unmodified, 50 comparisons against the system's own tools | diff --git a/examples/identifiers/mcpp.toml b/examples/identifiers/mcpp.toml new file mode 100644 index 0000000..0ca0166 --- /dev/null +++ b/examples/identifiers/mcpp.toml @@ -0,0 +1,13 @@ +[package] +name = "identifiers" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.identifiers] +kind = "bin" +main = "src/main.c" + +[build] +cxx_runtime = "host-coupled" diff --git a/examples/identifiers/src/main.c b/examples/identifiers/src/main.c new file mode 100644 index 0000000..43f596f --- /dev/null +++ b/examples/identifiers/src/main.c @@ -0,0 +1,52 @@ +/* Names this library must NOT take from a program above it. + * + * Reported as mcpplibs/openkal-musl#13: a program built on this package could + * not declare `hidden' as an ordinary identifier, because musl's INTERNAL + * header overlay defines it as an attribute and this package published the path + * it is built from. + * + * ⭐ THIS IS A COMPILE-TIME CRITERION AND IT IS THE WHOLE OF THE TEST. If any + * of the three below is still a macro, this file does not compile: in C they + * were emptied, so `static int hidden = 7;' becomes `static int = 7;'. There + * is nothing to run and nothing to compare --- the program exists so that + * something LINKS and RUNS afterwards, which is what distinguishes a source + * that compiles from a package that works. + * + * ⚠️ `restrict' IS NOT AMONG THEM, and its absence here is deliberate rather + * than an oversight. musl's PUBLIC headers write it --- declares + * `fprintf(FILE *restrict, const char *restrict, ...)' --- so a C++ program + * above this library needs the spelling defined and cannot have the name. That + * is a property of musl's headers and would be one above a kernel too. + * port/include/features.h records the division. + */ +#include + +static int hidden = 7; +static int weak = 11; +static int weak_alias = 23; + +/* And as a function parameter and a member, which is where a macro that + * survived would show differently. */ +struct holder { int hidden; int weak; }; + +static int sum(int hidden, int weak, int weak_alias) +{ + struct holder h = { hidden, weak }; + return h.hidden + h.weak + weak_alias; +} + +int main(void) +{ + setbuf(stdout, NULL); + const int total = sum(hidden, weak, weak_alias); + printf("hidden+weak+weak_alias = %d\n", total); + /* 41, and the value is printed rather than merely computed so that a + * reader of the log sees the three names carried a value of the program's + * own choosing rather than an attribute. */ + const int failures = (total == 41) ? 0 : 1; + if (failures) printf("FAIL: the three names did not carry their values\n"); + /* The same last line every probe in this repository prints, so that one + * runner reads all four. tools/run-probe.sh asserts both directions of it. */ + printf("-- failures: %d --\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/net/mcpp.toml b/examples/net/mcpp.toml new file mode 100644 index 0000000..1f08aae --- /dev/null +++ b/examples/net/mcpp.toml @@ -0,0 +1,16 @@ +[package] +name = "net" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.net] +kind = "bin" +main = "src/main.c" + +# What a program above this package must say, and why it is one line. The same +# statement examples/posix makes, for the same reason: a program above this +# package carries no other runtime, so the build tool must embed none. +[build] +cxx_runtime = "host-coupled" diff --git a/examples/net/src/main.c b/examples/net/src/main.c new file mode 100644 index 0000000..b2873b8 --- /dev/null +++ b/examples/net/src/main.c @@ -0,0 +1,199 @@ +/* A POSIX program that names no openkal symbol. + * + * ⭐ THAT IS THE WHOLE OF THE CRITERION. This package's claim is that a program + * written for POSIX runs above openkal without being rewritten, and a probe that + * called `kal_net_connect' to check that `connect' works would be checking the + * wrong thing. Nothing below is conditional on which implementation is beneath; + * the same source is what a program in the index would contain. + * + * What it exercises is three of the routes openkal 0.8 made possible and this + * port added: the socket family upon `openkal.net', the datagram family upon + * `openkal.datagram', and readiness upon `openkal.timeout'. Reported as + * mcpplibs/openkal-linux#13. The fourth --- duplicating the calling image upon + * `openkal.space' --- is examples/subprocess, because it is a different subject + * and its environment's answer may honestly be no. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int failures = 0; + +static void check(int ok, const char* what) +{ + if (!ok) { printf("FAIL: %s (errno=%d)\n", what, errno); failures++; } + else printf("ok: %s\n", what); +} + +/* The loopback address, written out. `inet_pton' would be a second thing under + * test, and this program is about the socket calls. */ +static void loopback(struct sockaddr_in* a, unsigned short port) +{ + memset(a, 0, sizeof *a); + a->sin_family = AF_INET; + a->sin_port = htons(port); + a->sin_addr.s_addr = htonl(0x7f000001u); +} + +int main(void) +{ + /* Unbuffered, so that a run that stops says how far it got. */ + setbuf(stdout, NULL); + + printf("-- openkal-musl network probe --\n"); + + /* --- a listener, and the endpoint the environment chose ----------------- */ + + const int lis = socket(AF_INET, SOCK_STREAM, 0); + check(lis >= 0, "a stream socket is made"); + if (lis < 0) { printf("-- failures: %d --\n", failures); return 1; } + + struct sockaddr_in want; + loopback(&want, 0); + check(bind(lis, (struct sockaddr*)&want, sizeof want) == 0, "it is bound to port zero"); + check(listen(lis, 4) == 0, "it listens"); + + /* ⭐ PORT ZERO ASKS THE ENVIRONMENT TO CHOOSE, and a program that must + * publish where it is listening has no other way to learn it. This is the + * enquiry openkal.net added `kal_net_listener_local' for. */ + struct sockaddr_in got; + socklen_t glen = sizeof got; + check(getsockname(lis, (struct sockaddr*)&got, &glen) == 0 + && glen == sizeof got && ntohs(got.sin_port) != 0, + "the port it was given is reported back"); + printf(" listening on 127.0.0.1:%u\n", (unsigned)ntohs(got.sin_port)); + + /* --- nothing is pending yet --------------------------------------------- */ + + struct pollfd pf = { lis, POLLIN, 0 }; + check(poll(&pf, 1, 0) == 0, "polling the listener reports nothing yet"); + + check(fcntl(lis, F_SETFL, O_NONBLOCK) == 0, "the listener is made non-blocking"); + errno = 0; + check(accept(lis, NULL, NULL) < 0 && errno == EAGAIN, + "a non-blocking accept with nothing pending reports EAGAIN"); + check(fcntl(lis, F_SETFL, 0) == 0, "and blocking again"); + + /* --- a connection ------------------------------------------------------- */ + + const int cli = socket(AF_INET, SOCK_STREAM, 0); + check(cli >= 0, "a second stream socket is made"); + check(connect(cli, (struct sockaddr*)&got, sizeof got) == 0, "it connects"); + + pf.revents = 0; + check(poll(&pf, 1, 2000) == 1 && (pf.revents & POLLIN), + "the listener is now readable"); + + struct sockaddr_in from; + socklen_t flen = sizeof from; + const int srv = accept(lis, (struct sockaddr*)&from, &flen); + check(srv >= 0, "the connection is accepted"); + check(from.sin_family == AF_INET && ntohl(from.sin_addr.s_addr) == 0x7f000001u, + "and the peer it names is the loopback address"); + + struct sockaddr_in peer; + socklen_t plen = sizeof peer; + check(getpeername(cli, (struct sockaddr*)&peer, &plen) == 0 + && ntohs(peer.sin_port) == ntohs(got.sin_port), + "the client's peer is the port it connected to"); + + /* --- bytes, both ways --------------------------------------------------- */ + + check(write(cli, "ping", 4) == 4, "four bytes are written to the connection"); + + char in[16]; + memset(in, 0, sizeof in); + /* ⚠️ READ IN A LOOP. A read of a connection may report fewer bytes than + * were sent, on every system --- and this port answers a readiness enquiry + * by holding one byte, so a poll before a read makes the short read the + * ordinary case rather than a rare one. A program that assumed otherwise + * would be asserting something POSIX does not promise. */ + size_t have = 0; + while (have < 4) { + const ssize_t r = read(srv, in + have, 4 - have); + if (r <= 0) break; + have += (size_t)r; + } + check(have == 4 && memcmp(in, "ping", 4) == 0, "and arrive at the other end"); + + check(send(srv, "pong", 4, 0) == 4, "four are sent back"); + memset(in, 0, sizeof in); + have = 0; + while (have < 4) { + const ssize_t r = recv(cli, in + have, 4 - have, 0); + if (r <= 0) break; + have += (size_t)r; + } + check(have == 4 && memcmp(in, "pong", 4) == 0, "and arrive back"); + + /* --- half-closure ------------------------------------------------------- */ + + check(shutdown(cli, SHUT_WR) == 0, "the client ends its half of the connection"); + const ssize_t eof = read(srv, in, sizeof in); + check(eof == 0, "and the server observes the end of input"); + + check(close(cli) == 0, "the client is closed"); + check(close(srv) == 0, "the server end is closed"); + check(close(lis) == 0, "the listener is closed"); + + /* --- datagrams ----------------------------------------------------------- */ + + const int a = socket(AF_INET, SOCK_DGRAM, 0); + const int b = socket(AF_INET, SOCK_DGRAM, 0); + check(a >= 0 && b >= 0, "two datagram sockets are made"); + + struct sockaddr_in abind; + loopback(&abind, 0); + check(bind(a, (struct sockaddr*)&abind, sizeof abind) == 0, "one is bound"); + socklen_t alen = sizeof abind; + check(getsockname(a, (struct sockaddr*)&abind, &alen) == 0 && ntohs(abind.sin_port) != 0, + "and reports the port it was given"); + + check(sendto(b, "dgram", 5, 0, (struct sockaddr*)&abind, sizeof abind) == 5, + "a message is sent to it"); + + struct pollfd df = { a, POLLIN, 0 }; + check(poll(&df, 1, 2000) == 1 && (df.revents & POLLIN), + "the receiving socket becomes readable"); + + char msg[32]; + struct sockaddr_in sender; + socklen_t slen = sizeof sender; + memset(msg, 0, sizeof msg); + const ssize_t got_n = recvfrom(a, msg, sizeof msg, 0, (struct sockaddr*)&sender, &slen); + /* ⚠️ THE MESSAGE ARRIVES WHOLE, WHICH IS THE PROPERTY THAT DISTINGUISHES A + * DATAGRAM FROM A STREAM. A loop here would hide a port that had split it. */ + check(got_n == 5 && memcmp(msg, "dgram", 5) == 0, "the whole message arrives at once"); + check(sender.sin_family == AF_INET && ntohl(sender.sin_addr.s_addr) == 0x7f000001u, + "and it names who sent it"); + + check(close(a) == 0 && close(b) == 0, "both datagram sockets are closed"); + + /* --- select over a set --------------------------------------------------- */ + + int pipefd[2]; + check(pipe(pipefd) == 0, "a pipe is made"); + check(write(pipefd[1], "x", 1) == 1, "a byte is put into it"); + + fd_set rd; + FD_ZERO(&rd); + FD_SET(pipefd[0], &rd); + struct timeval tv = { 2, 0 }; + check(select(pipefd[0] + 1, &rd, NULL, NULL, &tv) == 1 && FD_ISSET(pipefd[0], &rd), + "select reports the read end ready"); + char one = 0; + check(read(pipefd[0], &one, 1) == 1 && one == 'x', "and the byte is still there to read"); + close(pipefd[0]); + close(pipefd[1]); + + printf("-- failures: %d --\n", failures); + return failures ? 1 : 0; +} diff --git a/examples/subprocess/mcpp.toml b/examples/subprocess/mcpp.toml new file mode 100644 index 0000000..5e5ce2e --- /dev/null +++ b/examples/subprocess/mcpp.toml @@ -0,0 +1,13 @@ +[package] +name = "subprocess" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.subprocess] +kind = "bin" +main = "src/main.c" + +[build] +cxx_runtime = "host-coupled" diff --git a/examples/subprocess/src/main.c b/examples/subprocess/src/main.c new file mode 100644 index 0000000..3dc51f3 --- /dev/null +++ b/examples/subprocess/src/main.c @@ -0,0 +1,128 @@ +/* Another program, started three ways, by a program that names no openkal + * symbol. + * + * fork duplicating the calling image --- `openkal.space' + * system a command line handed to a shell --- `openkal.process' + * popen the same, with a channel back --- `openkal.process' twice over + * + * ⭐ TWO OF THE THREE ALREADY WORKED AND NOTHING SAID SO. This package replaces + * musl's `posix_spawn' with port/src/okm_spawn.c, and `system' and `popen' are + * both written on `posix_spawn' --- so they have been available for as long as + * that file has, and no test in this repository ever ran one. A capability that + * works and is not asserted is a capability that will stop working quietly; + * these three lines are what keeps that from happening again. + * + * ⚠️ WHAT IS EXPECTED IS STATED ON THE COMMAND LINE RATHER THAN INFERRED, which + * is the same arrangement examples/net uses and for the same reason. An + * environment whose backend declines `openkal.space' is not a failure; an + * environment that was expected to provide it and quietly does not IS one, and + * a probe that accepted either answer could not tell them apart. + * + * --fork | --no-fork whether the calling image can be duplicated + * --shell | --no-shell whether this system has a shell at /bin/sh + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +static int failures = 0; + +static void check(int ok, const char* what) +{ + if (!ok) { printf("FAIL: %s (errno=%d)\n", what, errno); failures++; } + else printf("ok: %s\n", what); +} + +int main(int argc, char** argv) +{ + setbuf(stdout, NULL); + + int expect_fork = -1, expect_shell = -1; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--fork") == 0) expect_fork = 1; + else if (strcmp(argv[i], "--no-fork") == 0) expect_fork = 0; + else if (strcmp(argv[i], "--shell") == 0) expect_shell = 1; + else if (strcmp(argv[i], "--no-shell") == 0) expect_shell = 0; + } + if (expect_fork < 0 || expect_shell < 0) { + printf("usage: subprocess --fork|--no-fork --shell|--no-shell\n"); + return 2; + } + + printf("-- openkal-musl subprocess probe --\n"); + + /* --- duplicating the calling image ---------------------------------------- */ + + errno = 0; + const pid_t kid = fork(); + if (kid == 0) { + /* ⚠️ `_exit' AND NOT `exit'. The copy holds a copy of the parent's stdio + * buffers, and running the exit handlers would write them a second + * time --- which is a defect of the probe and looks like one of the + * port. */ + _exit(23); + } + if (expect_fork) { + check(kid > 0, "the calling image is duplicated"); + if (kid > 0) { + int status = 0; + check(waitpid(kid, &status, 0) == kid, "the copy is awaited"); + const int ok = WIFEXITED(status) && WEXITSTATUS(status) == 23; + /* ⚠️ THE RAW STATUS IS PRINTED WHEN IT IS WRONG, and only then. A + * line reading "it did not report the status it was written to + * report" names a fault and not a place: a copy that ended on a + * signal and one that returned the wrong number are different + * failures, and the number is what tells them apart. */ + if (!ok) + printf(" status=0x%x exited=%d code=%d signalled=%d signal=%d\n", + (unsigned)status, WIFEXITED(status), WEXITSTATUS(status), + WIFSIGNALED(status), WTERMSIG(status)); + check(ok, "and it reported the status it was written to report"); + } else { + failures += 2; + } + } else { + /* ⭐ A REFUSAL IS THE EXPECTED ANSWER HERE AND IS CHECKED AS ONE. Clause + * 3 permits an implementation to decline an interface in whole, clause + * 6.1 makes the absence a link-time one, and okm_opt.h's rule turns it + * into the defined error a POSIX caller already handles. */ + check(kid < 0 && errno == ENOSYS, + "duplicating the calling image is refused, as this system requires"); + } + + if (!expect_shell) { + printf(" this system has no shell at /bin/sh; system and popen are not asked\n"); + printf("-- failures: %d --\n", failures); + return failures ? 1 : 0; + } + + /* --- a command line handed to a shell -------------------------------------- */ + + const int rc = system("exit 5"); + /* The status is in the shape `wait' reports: the low seven bits name a + * signal and are zero when the program ended by returning. */ + check(rc >= 0 && WIFEXITED(rc) && WEXITSTATUS(rc) == 5, + "a command line runs and its status comes back"); + + /* --- and the same, with a channel back ------------------------------------- */ + + FILE* f = popen("echo carried-back", "r"); + check(f != NULL, "a command line runs with a channel back"); + if (f) { + char line[128]; + memset(line, 0, sizeof line); + const int got = fgets(line, sizeof line, f) != NULL; + check(got && strncmp(line, "carried-back", 12) == 0, + "and what it wrote arrives through the channel"); + check(pclose(f) == 0, "the channel closes with the command's own status"); + } else { + failures += 2; + } + + printf("-- failures: %d --\n", failures); + return failures ? 1 : 0; +} diff --git a/mcpp.toml b/mcpp.toml index 97ad8af..f5cffbb 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.4.0" +version = "0.5.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" @@ -33,7 +33,7 @@ openkal = "0.8.0" openkal-linux = { version = "0.6.0", features = ["standalone"] } [target.'cfg(os = "macos")'.dependencies] -openkal-macos = { version = "0.4.0", features = ["standalone"] } +openkal-macos = { version = "0.5.0", features = ["standalone"] } # ⚠️ FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. # @@ -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.2.0", features = ["standalone"] } +openkal-windows = { version = "0.3.0", features = ["standalone"] } # The feature macros musl's own build establishes. # @@ -177,6 +177,15 @@ sources = [ # closure at the password functions. Withholding either takes an essential or # an ordinary facility with it, so neither is withheld. # + # ⭐ THREE OF THOSE FOUR ROWS ARE NOW MOOT, AND FOR THE RIGHT REASON: the + # facilities arrived rather than the exclusions being relaxed. openkal 0.8 + # added `net', `datagram', `timeout' and `space'; port/src/okm_net.c, + # okm_poll.c and okm_fork.c route them, so `select', the network family and + # `fork' are not facilities this port lacks and had to reason about keeping --- + # they work. `epoll' and the four descriptor-shaped notifications below stay + # withheld, because a readiness SET held by the environment is a facility of + # one kernel rather than a capability, and openkal has none to express. + # # ⚠️ AND THE MECHANISM HAS A PRECONDITION THIS PACKAGE MEETS ON ONE TARGET OF # FOUR. `-ffunction-sections` with `-Wl,--gc-sections` is what makes an # unreferenced definition cost nothing, and only `cfg(os = "linux")` sets those diff --git a/port/include/features.h b/port/include/features.h index 36aee3e..1d02eac 100644 --- a/port/include/features.h +++ b/port/include/features.h @@ -159,20 +159,59 @@ * defined breaks any consumer that writes * `__attribute__((weak_alias(...)))' of its own. LLVM's * libunwind does, in fifteen places, and reported - * `use of undeclared identifier __weak__'. */ + * `use of undeclared identifier __weak__'. + * + * ⚠️⚠️ THE FOUR DO NOT ALL COME FROM THE SAME PLACE, WHICH IS WHY THEY ARE NO + * LONGER TREATED THE SAME. Reported as mcpplibs/openkal-musl#13: a program + * above this library could not declare `static int hidden = 7;'. + * + * `restrict' is written by musl's PUBLIC headers. declares + * `fprintf(FILE *restrict, const char *restrict, ...)', so a C++ consumer + * needs the spelling whether or not the internal overlay is anywhere near + * its command line. It stays unconditional, and the name it costs a C++ + * program is a property of musl's public headers rather than of this port. + * + * `hidden', `weak' and `weak_alias' are written by the INTERNAL OVERLAY and + * by nothing else. Measured: `grep -w' over musl/include --- the whole of + * what a consumer reads --- finds `hidden' zero times and `weak' zero times. + * Nothing a consumer includes needs any of the three to be defined at all. + * + * ⇒ THE TEST IS THE OVERLAY'S OWN DEFINITION. src/include/features.h is what + * defines the three, and it is reached from here by `#include_next' only when + * that directory is on the command line. So `#ifdef hidden' below asks exactly + * the question this block was always trying to ask --- "did the overlay get + * included" --- rather than the question it used to ask, which was "is this + * unit musl's" and which answered the wrong way for every consumer. + * + * ⚠️ THE COMPILER-RT CASE STILL HOLDS, and it is the reason the block is scoped + * rather than deleted. A board building compiler-rt here compiles C that is not + * musl's WITH the overlay on its line --- `private_include_dirs' does not reach + * it, because it is not a consumer of this package's public interface but a + * build this package's own directories were handed to. `#ifdef hidden' is true + * there, and the three are neutralised there, exactly as before. + * + * ⇒ Two consumers, two answers, one test: + * + * ordinary consumer overlay absent `hidden' is the program's to use + * compiler-rt overlay present `hidden' neutralised, as measured */ # ifdef __cplusplus # if !defined(restrict) # define restrict __restrict # endif -# undef hidden -# define hidden extern "C" -# else -# undef hidden -# define hidden # endif -# undef weak -# define weak -# undef weak_alias + +# ifdef hidden +# ifdef __cplusplus +# undef hidden +# define hidden extern "C" +# else +# undef hidden +# define hidden +# endif +# undef weak +# define weak +# undef weak_alias +# endif /* hidden --- the internal overlay is on this command line */ #endif /* !OKM_MUSL_INTERNAL */ diff --git a/port/src/okm.h b/port/src/okm.h index a0c7054..5c26beb 100644 --- a/port/src/okm.h +++ b/port/src/okm.h @@ -82,6 +82,7 @@ #define OKM_MAX_FD 1024 #define OKM_MAX_DESC 512 +#define OKM_MAX_SOCK 128 #define OKM_MAX_PATH 4096 #define OKM_MAX_DIRS 64 #define OKM_DIR_PATH 512 @@ -98,6 +99,11 @@ enum okm_kind { OKM_CHANNEL, /* an OWNED stream, obtained from openkal.process */ OKM_FILE, /* an owned file obtained from openkal.fs */ OKM_DIR, /* an owned directory */ + /* A socket. Its state lives in the table in okm_net.c rather than here, + * because a socket passes through four states that no other descriptor has + * and carries two endpoints that no other descriptor needs. `sock' below is + * the index. */ + OKM_SOCKET, }; /* An open file description, in the POSIX sense: what a descriptor refers to, @@ -120,6 +126,28 @@ struct okm_desc { int pending; int pending_kind; char pending_name[256]; + int sock; /* OKM_SOCKET: the slot in okm_net.c, else -1 */ + /* ⭐ ONE BYTE READ AHEAD, WHICH IS HOW A READINESS ENQUIRY IS ANSWERED. + * + * openkal has no operation that reports whether a transfer would proceed. + * `openkal.timeout' bounds the transfer itself, and clause 6.3 records + * readiness notification among the mechanisms considered and NOT adopted: + * an interface reporting readiness would oblige every implementation to + * maintain a set and a context of its own. + * + * So `poll' is answered by attempting the transfer under a bound and + * keeping what it produced. A byte that arrived is a byte the descriptor + * has, which is what POLLIN asserts; holding it here is what makes the + * assertion true for the read that follows rather than merely true at the + * moment it was made. okm_desc already holds a directory entry for the same + * reason and by the same means. + * + * ⚠️ ONE BYTE AND NOT A BUFFER. A short read is a result every caller of + * `read' already handles, and a larger read-ahead would turn this into a + * second layer of buffering underneath stdio's. */ + int ahead; /* a byte is held */ + int ahead_eof; /* the bounded read reported end of input */ + unsigned char ahead_byte; }; /* The lock. Every operation upon the table is short, and contention is between @@ -178,6 +206,79 @@ int okm_chdir(int dirfd, const char* path); * reconstruct a namespace. */ int okm_errno(int kal_error_value); +/* --- sockets, in okm_net.c ------------------------------------------------- + * + * ⚠️ BSD SEPARATES `socket' FROM `connect' AND `bind'; openkal DOES NOT. + * `kal_net_connect' produces a connection and there is no unbound socket to + * produce first. So a descriptor made by `socket' holds nothing but the three + * numbers it was given, and the openkal operation happens later --- at + * `connect', or at `listen' once `bind' has recorded where. That deferral is + * the whole of the adaptation, and it lives in one file. + * + * Every one of these returns 0 or a count, or a negated errno value, which is + * the convention the dispatcher in okm_syscall.c passes straight through. */ +int okm_sock_open (int domain, int type, int protocol); +int okm_sock_bind (int fd, const void* addr, unsigned len); +int okm_sock_listen (int fd, int backlog); +int okm_sock_accept (int fd, void* addr, unsigned* len, int flags); +int okm_sock_connect(int fd, const void* addr, unsigned len); +int okm_sock_name (int fd, void* addr, unsigned* len, int peer); +int okm_sock_shutdown(int fd, int how); +long okm_sock_send (int fd, const void* buf, unsigned long len, int flags, + const void* addr, unsigned alen); +long okm_sock_recv (int fd, void* buf, unsigned long len, int flags, + void* addr, unsigned* alen); +int okm_sock_setopt (int fd, int level, int opt, const void* val, unsigned len); +int okm_sock_getopt (int fd, int level, int opt, void* val, unsigned* len); + +/* Released with the description that held it. Called from okm_fd.c, which is + * the one place a description's lifetime ends. */ +void okm_sock_release(int slot); + +/* Waits up to `ns' for a socket to have a connection to accept or a message to + * receive, and keeps what arrived so that the operation which follows finds it. + * 1 ready, 0 the bound expired, negative a negated errno value. */ +int okm_sock_wait_in(struct okm_desc* d, kal_u64 ns); + +/* Which of three shapes a socket descriptor has, so that the readiness code can + * ask without knowing the socket table. A connected socket is a stream and + * takes the same path a pipe takes; a listener and a datagram endpoint are + * neither, and each waits in its own way. */ +#define OKM_SOCK_SHAPE_IDLE 0 /* nothing can arrive on it yet */ +#define OKM_SOCK_SHAPE_STREAM 1 /* connected: the read-ahead applies */ +#define OKM_SOCK_SHAPE_OWN 2 /* a listener or a datagram endpoint */ +int okm_sock_shape(struct okm_desc* d); + +/* --- readiness and bounded transfer, in okm_poll.c ------------------------- */ + +/* The read-ahead a readiness enquiry left, delivered to a caller of `read'. + * + * > 0 the count placed in the buffer + * 0 nothing was held; the caller performs its own transfer + * OKM_AHEAD_EOF an end of input the enquiry already observed. The read is + * complete with zero bytes, and the mark is cleared: a + * terminal may deliver more after one, and a pipe reports + * the same end again at once, so neither is lost by + * forgetting it. */ +#define OKM_AHEAD_EOF (-1L) +long okm_take_ahead(struct okm_desc* d, void* buf, unsigned long len); + +/* A transfer bounded in time. `ns' of zero is openkal's spelling of "no bound" + * and is not what a non-blocking descriptor wants; `OKM_NOW_NS' is the smallest + * bound there is, and an environment with a coarse clock rounds it up to its + * own granularity rather than refusing it. Both report -ENOSYS where the + * environment beneath provides no `openkal.timeout'. */ +#define OKM_NOW_NS ((kal_u64)1) +long okm_timed_read (kal_uintptr stream, void* buf, unsigned long len, kal_u64 ns); +long okm_timed_write(kal_uintptr stream, const void* buf, unsigned long len, kal_u64 ns); + +/* Whether the environment beneath can bound an operation at all. `O_NONBLOCK' + * is refused where it cannot, rather than accepted and ignored. */ +int okm_can_bound(void); + +/* poll(2) over a set, and the whole of what `select' is expressed as. */ +long okm_poll(void* fds, unsigned long n, int timeout_ms); + /* The preopened directories, read once. */ int okm_preopen_count(void); int okm_preopen(int index, struct kal_dir* dir, const char** name, size_t* len); diff --git a/port/src/okm_fd.c b/port/src/okm_fd.c index 75ec61e..ddf3ddd 100644 --- a/port/src/okm_fd.c +++ b/port/src/okm_fd.c @@ -31,7 +31,7 @@ * operation that made it, so the test below can never fail in a program that has * one. */ #include -extern __typeof(kal_process_channel_close) kal_process_channel_close __attribute__((weak)); +extern __typeof(kal_process_channel_close) kal_process_channel_close __attribute__((__weak__)); #include "okm_opt.h" #include @@ -114,9 +114,13 @@ static void desc_release(int d) struct kal_stream s; s.h = p->stream; kal_process_channel_close(s); } + /* A socket's own release is the one openkal operation that depends on which + * of the four states it reached, so it is performed where those states are + * known rather than reproduced here. */ + else if (p->kind == OKM_SOCKET) okm_sock_release(p->sock); if (p->path_slot >= 0) g_dirpath_used[p->path_slot] = 0; p->kind = OKM_FREE; p->iter = 0; p->iter_open = 0; p->path_slot = -1; - p->pending = 0; + p->pending = 0; p->sock = -1; p->ahead = 0; p->ahead_eof = 0; } int okm_fd_alloc(int from) @@ -153,6 +157,7 @@ int okm_fd_bind(int fd, int kind, kal_uintptr stream, p->kind = kind; p->flags = flags; p->stream = stream; p->file = file; p->dir = dir; p->iter = 0; p->iter_open = 0; p->path_slot = -1; p->pending = 0; + p->sock = -1; p->ahead = 0; p->ahead_eof = 0; if (g_fd[fd].desc >= 0) desc_release(g_fd[fd].desc); g_fd[fd].desc = d; g_fd[fd].cloexec = (flags & O_CLOEXEC) ? 1 : 0; @@ -220,7 +225,7 @@ static size_t copy_str(char* dst, const char* src, size_t n, size_t cap) void okm_table_init(void) { for (int i = 0; i < OKM_MAX_FD; i++) { g_fd[i].desc = -1; g_fd[i].cloexec = 0; } - for (int i = 0; i < OKM_MAX_DESC; i++) g_desc[i].path_slot = -1; + for (int i = 0; i < OKM_MAX_DESC; i++) { g_desc[i].path_slot = -1; g_desc[i].sock = -1; } /* The program's own three streams are descriptors 0, 1 and 2, which is * what every program above this library assumes without saying so. They diff --git a/port/src/okm_fork.c b/port/src/okm_fork.c new file mode 100644 index 0000000..684662c --- /dev/null +++ b/port/src/okm_fork.c @@ -0,0 +1,166 @@ +/* fork(2), composed above openkal.space. + * + * ⭐ THE SPECIFICATION DESCRIBES THIS COMPOSITION AND DECLINES TO PERFORM IT, + * WHICH IS WHY THE CODE IS HERE AND NOT BENEATH. openkal/include/openkal/space.h + * says so in terms: + * + * A library above this interface reaches `fork' by saving its own execution + * state before the call and restoring it in the started context. That is + * composition, and it belongs above this line rather than in it: the saving + * is done with the compiler's own facilities, differs per architecture, and + * is not something a kernel interface can perform on a caller's behalf. + * + * This port is the library that composition was described for. The saving is + * `setjmp', whose per-architecture instruction sequences this port already + * carries in okm_setjmp.S, and the restoring is `longjmp' in the started + * context. Nothing beneath changes and the specification does not move. + * + * ⚠️ AN EARLIER READING OF openkal-linux#13 CONCLUDED THAT THE SPECIFICATION HAD + * DELIBERATELY DECLINED fork, on the ground that duplicating an address space + * AND ITS EXECUTION STATE cannot be required of every environment. Half of that + * is right and the conclusion drawn from it was wrong: clause 7.1 declines the + * PAIR, and `openkal.space' supplies the first half by itself. What was missing + * was never the atom. It was this file, and a `case SYS_clone' in the + * dispatcher. + * + * --- why the copied stack is the one the child returns on ------------------- + * + * `kal_space_start' calls `entry' in the copy. The copy contains this + * function's own frame at the same address it has here, because that is what + * copying an address space means --- so the context `setjmp' recorded is valid + * in the child, and `longjmp' unwinds to a frame that is OLDER than the one + * `entry' is standing on. That direction is the one longjmp is defined for. + * + * An implementation that gives the started context a stack of its own instead + * reaches the same place by the same route: `entry' stands on the stack this + * file supplies, and `longjmp' leaves it for the copied one. Both are correct + * and a caller cannot observe which happened, which is what the header says + * about the stack argument. + */ +#define _GNU_SOURCE +#include "okm.h" +#include "okm_opt.h" + +#include + +#include +#include +#include + +/* okm_context.c --- where the state belonging to one execution context is kept, + * keyed on the identity openkal gives that context. */ +uintptr_t __okm_get_tp(void); +void __okm_set_tp(uintptr_t); +void* __okm_get_self(void); +void __okm_set_self(void*); + +/* Weak, by the rule okm_net.c states: `openkal.space' is optional --- no + * bare-metal backend has a second address space to make --- and a strong + * reference would make a program that never calls `fork' fail to link. */ +extern __typeof(kal_space_start) kal_space_start __attribute__((__weak__)); + +int __okm_child_record(struct kal_process h); /* okm_syscall.c */ +void __okm_forget_children(void); /* okm_syscall.c */ + +/* The context the started child resumes into. + * + * ⚠️ ONE, AND THE PORT'S OWN LOCK AROUND IT. Two contexts forking at once would + * otherwise record over each other and the second child would resume into the + * first one's frame. This port's table lock is the one taken rather than a lock + * of this file's, and holding it does a second job worth having: the copy is + * taken while no other context is part-way through a change to the descriptor + * table, so the child begins with a table that is whole. + * + * ⚠️ THE CHILD RELEASES ITS COPY OF THE LOCK. It was taken before the copy, so + * the copy holds it too, and a child that did not release it would stop at the + * first descriptor it touched. */ +static jmp_buf g_resume; + +/* ⚠️⚠️ WHAT THE COPY CARRIES AND WHAT IT DOES NOT: THE IDENTITY IS NOT CARRIED. + * + * okm_context.c keeps this library's per-context state --- its error value, its + * locale, its thread record --- in a table keyed on `kal_task_current()'. The + * specification says that identity is "unique among contexts running at the same + * moment and may be reused after one ends". It says NOTHING about a copy of the + * address space, and two implementations answer differently: + * + * openkal-linux caches `gettid' in a thread-local, so the COPY of the + * cache answers the parent's value and the lookup succeeds + * openkal-macos asks `thread_selfid' every time, so the started context + * is a NEW thread of a NEW process and answers a value the + * table has never seen + * + * ⇒ The second is not a defect. It is the honest answer to the question the + * interface asks, and the assumption that a copy keeps its identity was this + * port's. + * + * ⭐ MEASURED, AND THE PORT'S OWN DIAGNOSTIC NAMED IT. On the macOS row the + * copy stopped with + * + * openkal-musl: this execution context has no per-context state --- the + * implementation's kal_task_current did not answer the same value here as + * it did when the context started + * + * which is the message `__okm_get_tp' has carried since it was written, for + * exactly this condition. Without it the report would have been a copy that + * ended on a signal, four layers from the cause. + * + * ⇒ The started context REBINDS ITS SLOT before anything reads per-context + * state. The values are read here, in the original, and are globals because a + * local written between `setjmp' and `longjmp' is indeterminate in the resumed + * context. */ +static volatile uintptr_t g_carried_tp; +static void* volatile g_carried_self; + +/* A stack for the entry function, used only by an implementation that honours + * the argument. All it holds is one call to `longjmp'. */ +static char g_entry_stack[8192] __attribute__((aligned(16))); + +static void child_entry(void* arg) +{ + (void)arg; + longjmp(g_resume, 1); +} + +/* Returns the child's identifier to the caller and zero to the child, which is + * what `fork' means and what no C function can do by returning once. */ +syscall_arg_t __okm_fork(void) +{ + if (!kal_space_start) return -ENOSYS; + + okm_lock(); + + g_carried_tp = __okm_get_tp(); + g_carried_self = __okm_get_self(); + + /* ⚠️ NOTHING BELOW THIS LINE MAY READ A LOCAL VARIABLE THAT WAS WRITTEN + * AFTER IT. A variable modified between `setjmp' and `longjmp' and not + * declared volatile is indeterminate in the resumed context; the child path + * therefore reads nothing but the two globals above and returns a constant. */ + if (setjmp(g_resume) != 0) { + /* ⚠️ THIS IS THE FIRST THING THE COPY DOES, AND THE ORDER IS THE POINT. + * `okm_unlock' is an atomic store and touches no per-context state; + * everything after it does. */ + __okm_set_tp(g_carried_tp); + __okm_set_self(g_carried_self); + /* The entries in the child table name programs the ORIGINAL started, + * and POSIX is explicit that a duplicate has no children. okm_syscall.c + * records what keeping them would cost. */ + __okm_forget_children(); + okm_unlock(); + return 0; /* the child */ + } + + struct kal_process child; + const int e = kal_space_start(child_entry, 0, + g_entry_stack + sizeof g_entry_stack, &child); + /* ⚠️ RELEASED BEFORE THE CHILD IS RECORDED, because the recording takes the + * same lock and this one does not nest. The child was copied above and + * holds its own copy of what was released here. */ + okm_unlock(); + if (e != kal_ok) return -okm_errno(e); + + const int pid = __okm_child_record(child); + if (pid < 0) { okm_process_close(child); return -EAGAIN; } + return (syscall_arg_t)pid; +} diff --git a/port/src/okm_net.c b/port/src/okm_net.c new file mode 100644 index 0000000..90c81e2 --- /dev/null +++ b/port/src/okm_net.c @@ -0,0 +1,788 @@ +/* Sockets, expressed over openkal.net and openkal.datagram. + * + * These two interfaces arrived in openkal 0.8 and nothing above them used them + * until now: a program built on this library reached `socket' and was told + * ENOSYS, because musl's network sources compile and issue system calls the + * dispatcher had no case for. Reported as mcpplibs/openkal-linux#13. The gap + * was in this port and not in musl's sources and not in the specification. + * + * ⚠️⚠️ THE ONE STRUCTURAL DIFFERENCE, AND EVERYTHING ELSE FOLLOWS FROM IT. + * + * BSD makes a socket first and decides what it is afterwards: `socket' yields + * an object, and `connect' or `bind'+`listen' then gives it a role. openkal + * does not have the intermediate object at all --- `kal_net_connect' produces a + * connection, `kal_net_listen' produces a listener, and there is no operation + * that produces neither. + * + * That is not a deficiency to work around. An unbound socket is a thing whose + * only capability is to become something else, and clause 3 admits an interface + * only for a capability that is minimal and universal. What is needed above is + * the DEFERRAL: a descriptor made by `socket' holds the three numbers it was + * given and nothing more, and the openkal operation happens at the call that + * says what the socket is for. `bind' records an endpoint; `listen' spends it; + * `connect' spends its own. + * + * ⇒ Four states, one table, one file. A caller cannot observe the deferral: + * every error a real kernel would report at `bind' this port reports at + * `listen', which is one call later and carries the same value. + * + * ⚠️⚠️ EVERY REFERENCE TO EITHER INTERFACE IS WEAK, AND THE RULE IS NOT + * OPTIONAL. Clause 6.1 expresses an interface an implementation does not + * provide as the ABSENCE of its definitions, and openkal-macos, openkal-windows, + * openkal-opensbi and openkal-uefi all decline these two today. A strong + * reference from here would turn "this backend has no network" into "no program + * above this library links", which is precisely the fault the same rule was + * added for `kal_process_channel' and `kal_random_fill' to avoid --- twice + * already, in this same port. + * + * ⭐ ONE TEST PER INTERFACE AND NOT ONE PER OPERATION. Clause 3 requires an + * implementation to provide an interface in whole or not at all, so whether + * `kal_net_connect' is present answers for all eleven names. Every name is + * still DECLARED weak --- that is what keeps the link from requiring it --- and + * exactly one is TESTED. + */ +#define _GNU_SOURCE +#include "okm.h" +#include "okm_opt.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* musl's internal overlay redirects these to its own entry points, and this + * file wants the public ones, exactly as okm_spawn.c does. */ +#undef malloc +#undef calloc +#undef realloc +#undef free + +/* --- the interfaces beneath, all weak -------------------------------------- */ + +extern __typeof(kal_net_connect) kal_net_connect __attribute__((__weak__)); +extern __typeof(kal_net_listen) kal_net_listen __attribute__((__weak__)); +extern __typeof(kal_net_accept) kal_net_accept __attribute__((__weak__)); +extern __typeof(kal_net_stream) kal_net_stream __attribute__((__weak__)); +extern __typeof(kal_net_peer) kal_net_peer __attribute__((__weak__)); +extern __typeof(kal_net_local) kal_net_local __attribute__((__weak__)); +extern __typeof(kal_net_listener_local) kal_net_listener_local __attribute__((__weak__)); +extern __typeof(kal_net_shutdown) kal_net_shutdown __attribute__((__weak__)); +extern __typeof(kal_net_close) kal_net_close __attribute__((__weak__)); +extern __typeof(kal_net_close_listener) kal_net_close_listener __attribute__((__weak__)); + +extern __typeof(kal_datagram_open) kal_datagram_open __attribute__((__weak__)); +extern __typeof(kal_datagram_local) kal_datagram_local __attribute__((__weak__)); +extern __typeof(kal_datagram_send_to) kal_datagram_send_to __attribute__((__weak__)); +extern __typeof(kal_datagram_recv_from) kal_datagram_recv_from __attribute__((__weak__)); +extern __typeof(kal_datagram_close) kal_datagram_close __attribute__((__weak__)); + +extern __typeof(kal_timeout_accept) kal_timeout_accept __attribute__((__weak__)); +extern __typeof(kal_timeout_recv_from) kal_timeout_recv_from __attribute__((__weak__)); + +#define OKM_HAVE_NET (kal_net_connect != 0) +#define OKM_HAVE_DGRAM (kal_datagram_open != 0) + +/* --- the table -------------------------------------------------------------- */ + +enum { + OKM_SOCK_FREE = 0, + OKM_SOCK_NEW, /* made; no openkal object yet */ + OKM_SOCK_LISTEN, /* kal_net_listen has been performed */ + OKM_SOCK_CONN, /* kal_net_connect, or a connection kal_net_accept gave */ + OKM_SOCK_DGRAM, /* kal_datagram_open has been performed */ +}; + +/* The largest message this port will hold on a datagram socket's behalf, which + * is the largest a datagram carries. A readiness enquiry has to RECEIVE the + * message to know there is one --- openkal has no enquiry that reports a + * message without taking it --- so the buffer is what makes the enquiry + * non-destructive. It is obtained when a datagram socket is first polled and + * not before, because a program that never polls never needs it. */ +#define OKM_DGRAM_MAX 65536 + +struct okm_sock { + int state; + int domain, type, protocol; + int have_local; /* `bind' recorded an endpoint */ + int have_peer; /* `connect' recorded one, for a datagram */ + struct kal_endpoint local, peer; + struct kal_net_conn conn; + struct kal_net_listener lis; + struct kal_datagram dg; + kal_u64 rcv_bound, snd_bound; /* SO_RCVTIMEO / SO_SNDTIMEO, nanoseconds */ + + /* What a readiness enquiry took and has not yet delivered. */ + int pend_conn; + struct kal_net_conn pend; + int pend_msg; + unsigned long pend_len; + struct kal_endpoint pend_from; + unsigned char* buf; +}; + +static struct okm_sock g_sock[OKM_MAX_SOCK]; + +static int slot_alloc(void) +{ + for (int i = 0; i < OKM_MAX_SOCK; i++) + if (g_sock[i].state == OKM_SOCK_FREE) { + struct okm_sock* s = &g_sock[i]; + unsigned char* keep = s->buf; /* the buffer outlives a slot */ + for (unsigned k = 0; k < sizeof *s; k++) ((char*)s)[k] = 0; + s->buf = keep; + s->state = OKM_SOCK_NEW; + return i; + } + return -1; +} + +static struct okm_sock* slot_of(struct okm_desc* d) +{ + if (!d || d->kind != OKM_SOCKET) return 0; + if (d->sock < 0 || d->sock >= OKM_MAX_SOCK) return 0; + return &g_sock[d->sock]; +} + +void okm_sock_release(int slot) +{ + if (slot < 0 || slot >= OKM_MAX_SOCK) return; + struct okm_sock* s = &g_sock[slot]; + switch (s->state) { + case OKM_SOCK_CONN: if (kal_net_close) kal_net_close(s->conn); break; + case OKM_SOCK_LISTEN: if (kal_net_close_listener) kal_net_close_listener(s->lis); break; + case OKM_SOCK_DGRAM: if (kal_datagram_close) kal_datagram_close(s->dg); break; + default: break; + } + /* A connection accepted to answer a readiness enquiry and never taken is + * still a connection, and closing the listener does not close it. */ + if (s->pend_conn && kal_net_close) kal_net_close(s->pend); + s->state = OKM_SOCK_FREE; + s->pend_conn = 0; s->pend_msg = 0; + /* ⚠️ THE BUFFER IS KEPT AND THE SLOT IS NOT. Freeing it here would return + * memory to an allocator this library also implements, from a path a + * program may reach while holding the table's lock. It is at most + * OKM_DGRAM_MAX per slot, the slot count is fixed, and the next socket to + * occupy the slot reuses it. */ +} + +/* --- addresses -------------------------------------------------------------- */ + +/* An endpoint is address bytes in network order plus a port in host order, and + * a `sockaddr' is the same two in this system's own layout. The conversion is + * written here rather than taken from `inet_pton' because it is a conversion + * between two structures and not a parse of text. + * + * ⚠️ A LENGTH OR A FAMILY THIS PORT DOES NOT KNOW IS REFUSED RATHER THAN READ + * AS ONE IT DOES, which is the rule the specification states for the same + * conversion in the other direction: an implementation that ignored the field + * would misread every address a later revision defines, silently. */ +static int to_endpoint(const void* addr, unsigned len, struct kal_endpoint* out) +{ + if (!addr || !out) return -EFAULT; + for (unsigned i = 0; i < sizeof out->addr; i++) out->addr[i] = 0; + out->addr_len = 0; + out->port = 0; + + const struct sockaddr* sa = (const struct sockaddr*)addr; + if (sa->sa_family == AF_INET) { + if (len < sizeof(struct sockaddr_in)) return -EINVAL; + const struct sockaddr_in* v4 = (const struct sockaddr_in*)addr; + const unsigned char* b = (const unsigned char*)&v4->sin_addr.s_addr; + for (int i = 0; i < 4; i++) out->addr[i] = b[i]; + out->addr_len = 4; + out->port = (kal_u32)ntohs(v4->sin_port); + return 0; + } + if (sa->sa_family == AF_INET6) { + if (len < sizeof(struct sockaddr_in6)) return -EINVAL; + const struct sockaddr_in6* v6 = (const struct sockaddr_in6*)addr; + for (int i = 0; i < 16; i++) out->addr[i] = v6->sin6_addr.s6_addr[i]; + /* Twenty bytes is the address followed by a scope identifier, and the + * shorter form denotes the same address when the scope is zero. Sending + * the shorter one where it suffices keeps an address that arrived as + * sixteen bytes going back out as sixteen. */ + if (v6->sin6_scope_id != 0) { + const kal_u32 sc = v6->sin6_scope_id; + for (int i = 0; i < 4; i++) + out->addr[16 + i] = (kal_u8)((sc >> (i * 8)) & 0xffu); + out->addr_len = 20; + } else { + out->addr_len = 16; + } + out->port = (kal_u32)ntohs(v6->sin6_port); + return 0; + } + /* AF_UNIX and everything else. openkal names an endpoint by address and + * port and has no other form, so a family this port cannot express is + * refused at the point of the attempt. */ + return -EAFNOSUPPORT; +} + +/* Writes an endpoint into a caller's `sockaddr', truncating as `accept' and + * `getsockname' are specified to, and reporting the length the whole address + * would have taken. */ +static int from_endpoint(const struct kal_endpoint* ep, void* addr, unsigned* len) +{ + if (!addr || !len) return 0; /* both optional, together and apart */ + + union { struct sockaddr_in v4; struct sockaddr_in6 v6; } u; + unsigned whole; + + for (unsigned i = 0; i < sizeof u; i++) ((char*)&u)[i] = 0; + + if (ep->addr_len == 4) { + u.v4.sin_family = AF_INET; + u.v4.sin_port = htons((unsigned short)ep->port); + unsigned char* b = (unsigned char*)&u.v4.sin_addr.s_addr; + for (int i = 0; i < 4; i++) b[i] = ep->addr[i]; + whole = sizeof u.v4; + } else if (ep->addr_len == 16 || ep->addr_len == 20) { + u.v6.sin6_family = AF_INET6; + u.v6.sin6_port = htons((unsigned short)ep->port); + for (int i = 0; i < 16; i++) u.v6.sin6_addr.s6_addr[i] = ep->addr[i]; + if (ep->addr_len == 20) { + kal_u32 sc = 0; + for (int i = 0; i < 4; i++) sc |= (kal_u32)ep->addr[16 + i] << (i * 8); + u.v6.sin6_scope_id = sc; + } + whole = sizeof u.v6; + } else { + /* An implementation reported a length this port does not know. The + * transfer that produced it still happened; what is unknown is who the + * other end was, and the caller is told that rather than given bytes. */ + *len = 0; + return -EAFNOSUPPORT; + } + + unsigned room = *len; + if (room > whole) room = whole; + for (unsigned i = 0; i < room; i++) ((char*)addr)[i] = ((const char*)&u)[i]; + *len = whole; + return 0; +} + +/* --- the calls -------------------------------------------------------------- */ + +int okm_sock_open(int domain, int type, int protocol) +{ + if (domain != AF_INET && domain != AF_INET6) return -EAFNOSUPPORT; + + const int base = type & ~(SOCK_CLOEXEC | SOCK_NONBLOCK); + if (base != SOCK_STREAM && base != SOCK_DGRAM) return -ESOCKTNOSUPPORT; + if (base == SOCK_STREAM && !OKM_HAVE_NET) return -EAFNOSUPPORT; + if (base == SOCK_DGRAM && !OKM_HAVE_DGRAM) return -EAFNOSUPPORT; + + /* ⚠️ REFUSED HERE RATHER THAN IGNORED. A descriptor that was asked to be + * non-blocking and is not would make every subsequent operation block where + * the caller arranged not to, and it would do so silently. */ + if ((type & SOCK_NONBLOCK) && !okm_can_bound()) return -ENOSYS; + + okm_lock(); + const int fd = okm_fd_alloc(0); + if (fd < 0) { okm_unlock(); return fd; } + const int slot = slot_alloc(); + if (slot < 0) { okm_unlock(); return -ENFILE; } + + struct kal_file nof = { 0 }; + struct kal_dir nod = { 0 }; + int flags = O_RDWR; + if (type & SOCK_NONBLOCK) flags |= O_NONBLOCK; + if (type & SOCK_CLOEXEC) flags |= O_CLOEXEC; + okm_fd_bind(fd, OKM_SOCKET, 0, nof, nod, flags); + + struct okm_desc* d = okm_desc_of(fd); + d->sock = slot; + g_sock[slot].domain = domain; + g_sock[slot].type = base; + g_sock[slot].protocol = protocol; + okm_unlock(); + return fd; +} + +int okm_sock_bind(int fd, const void* addr, unsigned len) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (s->state != OKM_SOCK_NEW) return -EINVAL; + + struct kal_endpoint ep; + const int r = to_endpoint(addr, len, &ep); + if (r) return r; + + /* ⭐ RECORDED AND NOT PERFORMED, which is the deferral this file exists for. + * A datagram socket spends it at the first send or receive; a stream socket + * spends it at `listen'. Neither loses an error: openkal reports at the + * operation what a kernel reports at the bind, and the value is the same + * one call later. */ + s->local = ep; + s->have_local = 1; + + if (s->type == SOCK_DGRAM) { + if (!OKM_HAVE_DGRAM) return -ENOSYS; + const int e = kal_datagram_open(&ep, &s->dg); + if (e != kal_ok) return -okm_errno(e); + s->state = OKM_SOCK_DGRAM; + } + return 0; +} + +int okm_sock_listen(int fd, int backlog) +{ + (void)backlog; /* openkal names no depth and a caller cannot state one */ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (s->type != SOCK_STREAM) return -EOPNOTSUPP; + if (s->state == OKM_SOCK_LISTEN) return 0; + if (s->state != OKM_SOCK_NEW) return -EINVAL; + if (!OKM_HAVE_NET) return -ENOSYS; + + /* A `listen' without a `bind' asks the environment to choose everything. + * openkal has no such form --- an endpoint is required --- so the wildcard + * address and port zero are named, which is what the absent bind meant. */ + struct kal_endpoint ep = s->local; + if (!s->have_local) { + for (unsigned i = 0; i < sizeof ep.addr; i++) ep.addr[i] = 0; + ep.addr_len = (s->domain == AF_INET6) ? 16 : 4; + ep.port = 0; + } + + const int e = kal_net_listen(&ep, &s->lis); + if (e != kal_ok) return -okm_errno(e); + s->state = OKM_SOCK_LISTEN; + return 0; +} + +/* Places a connection into a descriptor of its own. The lock is held. */ +static int adopt(struct kal_net_conn c, int flags) +{ + const int fd = okm_fd_alloc(0); + if (fd < 0) { kal_net_close(c); return fd; } + const int slot = slot_alloc(); + if (slot < 0) { kal_net_close(c); return -ENFILE; } + + 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); + struct okm_desc* nd = okm_desc_of(fd); + nd->sock = slot; + g_sock[slot].state = OKM_SOCK_CONN; + g_sock[slot].conn = c; + g_sock[slot].type = SOCK_STREAM; + return fd; +} + +int okm_sock_accept(int fd, void* addr, unsigned* len, int flags) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (s->state != OKM_SOCK_LISTEN) return -EINVAL; + if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; + if ((flags & SOCK_NONBLOCK) && !okm_can_bound()) return -ENOSYS; + + struct kal_net_conn c; + if (s->pend_conn) { + /* A readiness enquiry already accepted one. Delivering it here is what + * makes that enquiry's answer true rather than momentary. */ + c = s->pend; + s->pend_conn = 0; + } else if (d->flags & O_NONBLOCK) { + if (!kal_timeout_accept) return -ENOSYS; + const int e = kal_timeout_accept(s->lis, OKM_NOW_NS, &c); + if (e == kal_err_again) return -EAGAIN; + if (e != kal_ok) return -okm_errno(e); + } else { + const int e = kal_net_accept(s->lis, &c); + if (e != kal_ok) return -okm_errno(e); + } + + int newflags = 0; + if (flags & SOCK_NONBLOCK) newflags |= O_NONBLOCK; + if (flags & SOCK_CLOEXEC) newflags |= O_CLOEXEC; + + okm_lock(); + const int nfd = adopt(c, newflags); + if (nfd >= 0 && (flags & SOCK_CLOEXEC)) okm_fd_cloexec(nfd, 1); + okm_unlock(); + if (nfd < 0) return nfd; + + if (addr && len) { + struct kal_endpoint peer; + if (kal_net_peer(c, &peer) == kal_ok) from_endpoint(&peer, addr, len); + else *len = 0; + } + return nfd; +} + +int okm_sock_connect(int fd, const void* addr, unsigned len) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + + struct kal_endpoint ep; + const int r = to_endpoint(addr, len, &ep); + if (r) return r; + + if (s->type == SOCK_DGRAM) { + /* On a datagram socket `connect' names a default peer and sends + * nothing. The endpoint is recorded and the socket is opened if `bind' + * had not already opened it. */ + if (!OKM_HAVE_DGRAM) return -ENOSYS; + if (s->state == OKM_SOCK_NEW) { + const int e = kal_datagram_open(s->have_local ? &s->local : 0, &s->dg); + if (e != kal_ok) return -okm_errno(e); + s->state = OKM_SOCK_DGRAM; + } + s->peer = ep; + s->have_peer = 1; + return 0; + } + + if (s->state == OKM_SOCK_CONN) return -EISCONN; + if (s->state != OKM_SOCK_NEW) return -EINVAL; + if (!OKM_HAVE_NET) return -ENOSYS; + + /* ⚠️ THE CONNECTION IS ESTABLISHED BEFORE THIS RETURNS, EVEN ON A + * NON-BLOCKING DESCRIPTOR, and a caller cannot be told otherwise honestly. + * + * `kal_net_connect' completes or fails; openkal has no form that begins a + * connection and reports its outcome later, and clause 6.3 records + * readiness notification among the mechanisms considered and not adopted. + * A port that returned EINPROGRESS here would be promising a completion + * that nothing can report --- a caller would then poll for POLLOUT, be told + * ready, ask SO_ERROR and be told zero, all of it invented. + * + * So the call blocks and reports the truth. What a non-blocking caller + * loses is the overlap, not the outcome; what it would have lost the other + * way is the outcome itself. Recorded in musl/PATCHES.md. */ + const int e = kal_net_connect(&ep, &s->conn); + if (e != kal_ok) return -okm_errno(e); + s->state = OKM_SOCK_CONN; + s->peer = ep; + s->have_peer = 1; + d->stream = kal_net_stream(s->conn); + return 0; +} + +int okm_sock_name(int fd, void* addr, unsigned* len, int peer) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (!addr || !len) return -EFAULT; + + struct kal_endpoint ep; + int e = kal_err_not_supported; + + if (peer) { + if (s->state == OKM_SOCK_CONN) e = kal_net_peer(s->conn, &ep); + else if (s->have_peer) { ep = s->peer; e = kal_ok; } + else return -ENOTCONN; + } else { + switch (s->state) { + case OKM_SOCK_CONN: e = kal_net_local(s->conn, &ep); break; + case OKM_SOCK_LISTEN: e = kal_net_listener_local(s->lis, &ep); break; + case OKM_SOCK_DGRAM: e = kal_datagram_local(s->dg, &ep); break; + default: + /* Nothing has been opened, so there is nothing the environment has + * assigned. What `bind' recorded is the whole of the answer, and a + * socket that has not been bound either has none. */ + if (!s->have_local) return -EINVAL; + ep = s->local; + e = kal_ok; + break; + } + } + if (e != kal_ok) return -okm_errno(e); + return from_endpoint(&ep, addr, len); +} + +int okm_sock_shutdown(int fd, int how) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (s->state != OKM_SOCK_CONN) return -ENOTCONN; + + /* This system numbers the directions from zero and openkal from one, so the + * mapping is written out rather than computed: a number neither defines + * would otherwise be read as SHUT_RD. */ + int dir; + switch (how) { + case SHUT_RD: dir = KAL_SHUT_READ; break; + case SHUT_WR: dir = KAL_SHUT_WRITE; break; + case SHUT_RDWR: dir = KAL_SHUT_BOTH; break; + default: return -EINVAL; + } + const int e = kal_net_shutdown(s->conn, dir); + return e == kal_ok ? 0 : -okm_errno(e); +} + +/* Which bound an operation on this descriptor carries: the one a socket option + * asked for, or the smallest there is when the descriptor is non-blocking, or + * none. Zero is openkal's spelling of "no bound". */ +static kal_u64 bound_of(struct okm_desc* d, struct okm_sock* s, int flags, int receiving) +{ + if ((flags & MSG_DONTWAIT) || (d->flags & O_NONBLOCK)) return OKM_NOW_NS; + return receiving ? s->rcv_bound : s->snd_bound; +} + +long okm_sock_send(int fd, const void* buf, unsigned long len, int flags, + const void* addr, unsigned alen) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + + /* MSG_NOSIGNAL asks that a signal not be raised. There are no signals here, + * so the request is satisfied by there being nothing to raise --- the same + * shape as masking a set of signals that cannot arrive. Every other flag + * names a facility openkal does not have. */ + const int rest = flags & ~(MSG_NOSIGNAL | MSG_DONTWAIT); + if (rest) return -ENOSYS; + + if (s->type == SOCK_DGRAM) { + if (!OKM_HAVE_DGRAM) return -ENOSYS; + struct kal_endpoint to; + if (addr) { + const int r = to_endpoint(addr, alen, &to); + if (r) return r; + } else if (s->have_peer) { + to = s->peer; + } else { + return -EDESTADDRREQ; + } + if (s->state == OKM_SOCK_NEW) { + const int e = kal_datagram_open(s->have_local ? &s->local : 0, &s->dg); + 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; + } + + if (s->state != OKM_SOCK_CONN) return -ENOTCONN; + if (addr) return -EISCONN; + const kal_u64 ns = bound_of(d, s, flags, 0); + 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; +} + +long okm_sock_recv(int fd, void* buf, unsigned long len, int flags, + void* addr, unsigned* alen) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + + const int rest = flags & ~(MSG_NOSIGNAL | MSG_DONTWAIT); + if (rest) return -ENOSYS; /* MSG_PEEK, MSG_OOB, MSG_WAITALL, MSG_TRUNC */ + + if (s->type == SOCK_DGRAM) { + if (!OKM_HAVE_DGRAM) return -ENOSYS; + if (s->state == OKM_SOCK_NEW) { + const int e = kal_datagram_open(s->have_local ? &s->local : 0, &s->dg); + if (e != kal_ok) return -okm_errno(e); + s->state = OKM_SOCK_DGRAM; + } + + struct kal_endpoint from; + unsigned long got; + + if (s->pend_msg) { + /* The message a readiness enquiry took. Excess beyond the caller's + * buffer is lost, which is what the medium does and what this + * interface states. */ + got = s->pend_len < len ? s->pend_len : len; + for (unsigned long i = 0; i < got; i++) ((char*)buf)[i] = (char)s->buf[i]; + from = s->pend_from; + s->pend_msg = 0; + } else { + const kal_u64 ns = bound_of(d, s, flags, 1); + struct kal_io_result 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; + } 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 (addr && alen) from_endpoint(&from, addr, alen); + return (long)got; + } + + if (s->state != OKM_SOCK_CONN) return -ENOTCONN; + if (addr && alen) *alen = 0; + + const long held = okm_take_ahead(d, buf, len); + if (held == OKM_AHEAD_EOF) return 0; + if (held) return held; + + const kal_u64 ns = bound_of(d, s, flags, 1); + 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; +} + +/* --- options ---------------------------------------------------------------- + * + * ⚠️ AN OPTION THIS PORT CANNOT HONOUR IS REFUSED. Accepting one and ignoring it + * is the single failure this whole port is written to avoid: a caller that set + * SO_BROADCAST and was told it succeeded would send to a broadcast address and + * be told the send succeeded too, and nothing would ever arrive. ENOPROTOOPT is + * the value this system already defines for an option a socket does not have, + * and every caller of `setsockopt' handles a failure. + * + * Three are honoured because openkal makes them true, and one is accepted + * because it is ALREADY true --- which is a different statement from ignoring + * it, and is recorded where it is made. */ + +static kal_u64 ns_of_timeval(const void* val, unsigned len) +{ + if (len < sizeof(struct timeval)) return (kal_u64)-1; + const struct timeval* tv = (const struct timeval*)val; + if (tv->tv_sec == 0 && tv->tv_usec == 0) return 0; /* no bound */ + kal_u64 ns = (kal_u64)tv->tv_sec * 1000000000ull + (kal_u64)tv->tv_usec * 1000ull; + return ns ? ns : OKM_NOW_NS; +} + +int okm_sock_setopt(int fd, int level, int opt, const void* val, unsigned len) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (level != SOL_SOCKET) return -ENOPROTOOPT; + + switch (opt) { + case SO_REUSEADDR: + /* ⭐ ACCEPTED BECAUSE IT IS ALREADY IN EFFECT, not because it is + * harmless. openkal's `kal_net_listen' sets this on the listener it + * makes --- openkal-linux/src/net.cpp says so and gives the reason: a + * program restarted within the kernel's lingering interval is the + * ordinary case. A caller asking for it is asking for what it has. */ + return 0; + case SO_RCVTIMEO: + case SO_SNDTIMEO: { + const kal_u64 ns = ns_of_timeval(val, len); + if (ns == (kal_u64)-1) return -EINVAL; + if (ns && !okm_can_bound()) return -ENOSYS; + if (opt == SO_RCVTIMEO) s->rcv_bound = ns; else s->snd_bound = ns; + return 0; + } + default: + return -ENOPROTOOPT; + } +} + +int okm_sock_getopt(int fd, int level, int opt, void* val, unsigned* len) +{ + struct okm_desc* d = okm_desc_of(fd); + struct okm_sock* s = slot_of(d); + if (!s) return d ? -ENOTSOCK : -EBADF; + if (level != SOL_SOCKET) return -ENOPROTOOPT; + if (!val || !len) return -EFAULT; + + switch (opt) { + case SO_TYPE: + if (*len < sizeof(int)) return -EINVAL; + *(int*)val = s->type; + *len = sizeof(int); + return 0; + case SO_ERROR: + /* ⭐ ALWAYS ZERO, AND IT IS AN ACCURATE ANSWER RATHER THAN A STAND-IN. + * This option reports an error that arrived after the call that would + * have reported it returned. Every operation here completes before it + * returns --- see the note at `connect' --- so there is never one + * outstanding, and zero is what "no error is pending" means. */ + if (*len < sizeof(int)) return -EINVAL; + *(int*)val = 0; + *len = sizeof(int); + return 0; + case SO_RCVTIMEO: + case SO_SNDTIMEO: { + if (*len < sizeof(struct timeval)) return -EINVAL; + const kal_u64 ns = (opt == SO_RCVTIMEO) ? s->rcv_bound : s->snd_bound; + struct timeval* tv = (struct timeval*)val; + tv->tv_sec = (time_t)(ns / 1000000000ull); + tv->tv_usec = (suseconds_t)((ns % 1000000000ull) / 1000ull); + *len = sizeof *tv; + return 0; + } + default: + return -ENOPROTOOPT; + } +} + +/* --- readiness --------------------------------------------------------------- */ + +int okm_sock_shape(struct okm_desc* d) +{ + struct okm_sock* s = slot_of(d); + if (!s) return OKM_SOCK_SHAPE_IDLE; + if (s->state == OKM_SOCK_CONN) return OKM_SOCK_SHAPE_STREAM; + if (s->state == OKM_SOCK_LISTEN || s->state == OKM_SOCK_DGRAM) + return OKM_SOCK_SHAPE_OWN; + return OKM_SOCK_SHAPE_IDLE; +} + +int okm_sock_wait_in(struct okm_desc* d, kal_u64 ns) +{ + struct okm_sock* s = slot_of(d); + if (!s) return -EBADF; + + if (s->state == OKM_SOCK_LISTEN) { + if (s->pend_conn) return 1; + if (!kal_timeout_accept) return -ENOSYS; + struct kal_net_conn c; + const int e = kal_timeout_accept(s->lis, ns, &c); + if (e == kal_err_again) return 0; + if (e != kal_ok) return -okm_errno(e); + s->pend = c; + s->pend_conn = 1; + return 1; + } + + if (s->state == OKM_SOCK_DGRAM) { + if (s->pend_msg) return 1; + if (!kal_timeout_recv_from) return -ENOSYS; + /* ⚠️ THE BUFFER IS WHY A READINESS ENQUIRY IS NOT DESTRUCTIVE HERE. + * openkal reports a message by delivering it, so the only way to learn + * that one has arrived is to take it, and the only way to keep the + * enquiry honest is to hold it until the receive that follows. */ + if (!s->buf) { + s->buf = (unsigned char*)malloc(OKM_DGRAM_MAX); + if (!s->buf) return -ENOMEM; + } + struct kal_endpoint from; + const struct kal_io_result 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; + s->pend_from = from; + s->pend_msg = 1; + return 1; + } + + return 0; /* nothing can arrive on a socket that is neither */ +} diff --git a/port/src/okm_poll.c b/port/src/okm_poll.c new file mode 100644 index 0000000..f17d2b9 --- /dev/null +++ b/port/src/okm_poll.c @@ -0,0 +1,256 @@ +/* Readiness, and transfers bounded in time. + * + * ⚠️⚠️ openkal HAS NO OPERATION THAT REPORTS WHETHER A TRANSFER WOULD PROCEED, + * AND THAT IS DELIBERATE. Clause 6.3 records readiness notification among the + * mechanisms considered and NOT adopted: an interface reporting readiness + * obliges every implementation of it to maintain a set and a context of its + * own, which is a mechanism reconstructed rather than a facility conveyed. What + * openkal has instead is `openkal.timeout' --- the same operations with a bound + * added --- and openkal-linux's own timeout.cpp states the consequence in one + * line: "a bounded read is a bounded wait for readiness followed by the + * ordinary read". + * + * ⭐ SO `poll' IS ANSWERED BY ATTEMPTING THE TRANSFER AND KEEPING WHAT IT + * PRODUCED. One byte from a stream, one connection from a listener, one message + * from a datagram endpoint --- held in the descriptor and delivered to the + * operation that follows. POLLIN asserts that a read will not block, and a byte + * already in hand is the strongest form that assertion can take: it is true + * when the enquiry is answered and still true when the read happens, which a + * report about the state of a queue is not. + * + * This port already answers `getdents' the same way, and for the same reason: + * openkal has no operation that returns an entry to an iterator, so an entry + * that does not fit the caller's buffer is held rather than lost. + * + * ⚠️ WHAT IS NOT FAITHFUL, STATED HERE RATHER THAN DISCOVERED. + * + * POLLOUT is reported for every descriptor that may be written, without an + * enquiry, because there is nothing to enquire of. A bounded write bounds the + * WAIT and not the TRANSFER --- openkal/include/openkal/timeout.h says so in + * terms --- so a write this port begins completes or reports, and there is no + * state in which it "would block" that openkal can be asked about. A program + * that polls for POLLOUT to avoid blocking may therefore block in the write. + * Recorded in musl/PATCHES.md. + * + * POLLPRI, POLLRDBAND and the rest name out-of-band data, which openkal does + * not have. They are never reported. + * + * A set larger than one descriptor is waited upon by asking each in turn + * under the smallest bound there is, and repeating. That is O(n) per round + * and is what an interface without a readiness set permits; `epoll' remains + * withheld, being a facility of one kernel rather than a capability. + */ +#define _GNU_SOURCE +#include "okm.h" +#include "okm_opt.h" + +#include + +#include +#include +#include +#include + +/* Weak, by the rule okm_net.c states: `openkal.timeout' is optional, and a + * strong reference would make a backend that declines it one no program above + * this library could link against. */ +extern __typeof(kal_timeout_read) kal_timeout_read __attribute__((__weak__)); +extern __typeof(kal_timeout_write) kal_timeout_write __attribute__((__weak__)); + +int okm_can_bound(void) { return kal_timeout_read != 0; } + +/* --- bounded transfer -------------------------------------------------------- */ + +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; +} + +long okm_timed_write(kal_uintptr stream, const void* buf, unsigned long len, kal_u64 ns) +{ + 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; +} + +/* --- the read-ahead ---------------------------------------------------------- */ + +long okm_take_ahead(struct okm_desc* d, void* buf, unsigned long len) +{ + if (!d) return 0; + if (d->ahead && len) { + ((unsigned char*)buf)[0] = d->ahead_byte; + d->ahead = 0; + return 1; + } + if (d->ahead_eof) { d->ahead_eof = 0; return OKM_AHEAD_EOF; } + return 0; +} + +/* Whether a descriptor can have input at all, and by which route. */ +static int has_input_route(struct okm_desc* d) +{ + switch (d->kind) { + case OKM_STREAM: + case OKM_CHANNEL: + return (d->flags & O_ACCMODE) != O_WRONLY; + case OKM_SOCKET: + return okm_sock_shape(d) != OKM_SOCK_SHAPE_IDLE; + default: + return 0; + } +} + +/* Waits up to `ns' for a descriptor to have input, and keeps what arrived. + * 1 ready, 0 the bound expired, negative a negated errno value. */ +static int wait_in(struct okm_desc* d, kal_u64 ns) +{ + if (d->ahead || d->ahead_eof) return 1; + + if (d->kind == OKM_SOCKET && okm_sock_shape(d) == OKM_SOCK_SHAPE_OWN) + return okm_sock_wait_in(d, ns); + + if (!kal_timeout_read) return -ENOSYS; + + 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) { + /* 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. */ + d->ahead_eof = 1; + return 1; + } + d->ahead = 1; + d->ahead_byte = byte; + return 1; +} + +/* --- poll -------------------------------------------------------------------- */ + +/* The interval a set of more than one descriptor is revisited at. It is the + * granularity openkal-linux reports for its own bounded operations, and the + * interval its `kal_timeout_wait_process' polls a child at; matching it means + * this loop costs what that one costs rather than adding a second figure. */ +#define OKM_ROUND_NS 1000000ull + +static void sleep_ns(kal_u64 ns) +{ + struct timespec ts; + ts.tv_sec = (time_t)(ns / 1000000000ull); + ts.tv_nsec = (long)(ns % 1000000000ull); + nanosleep(&ts, 0); +} + +long okm_poll(void* p, unsigned long n, int timeout_ms) +{ + struct pollfd* fds = (struct pollfd*)p; + if (n && !fds) return -EFAULT; + if (n > OKM_MAX_FD) return -EINVAL; + + const int forever = timeout_ms < 0; + kal_u64 left = forever ? 0 : (kal_u64)timeout_ms * 1000000ull; + + /* ⭐ A SET OF ONE IS THE CASE WORTH SEPARATING, AND IT IS THE COMMON ONE. + * With a single descriptor the caller's whole bound can be handed to the + * one bounded operation, so the wait happens in the environment rather than + * in this loop. Only a larger set has to be revisited. */ + const int single = (n == 1) && (fds[0].fd >= 0); + + for (;;) { + long ready = 0; + long failure = 0; + int waited = 0; + + for (unsigned long i = 0; i < n; i++) { + fds[i].revents = 0; + if (fds[i].fd < 0) continue; + + struct okm_desc* d = okm_desc_of(fds[i].fd); + if (!d) { fds[i].revents = POLLNVAL; ready++; continue; } + + /* A file and a directory are always ready, which is what every + * environment reports for them: a read from a file does not wait. */ + if (d->kind == OKM_FILE || d->kind == OKM_DIR) { + if (fds[i].events & POLLIN) fds[i].revents |= POLLIN; + if (fds[i].events & POLLOUT) fds[i].revents |= POLLOUT; + if (fds[i].revents) ready++; + continue; + } + + if ((fds[i].events & POLLIN) && has_input_route(d)) { + /* ⚠️⚠️ ZERO MEANS THE OPPOSITE IN THE TWO INTERFACES, AND THE + * COLLISION IS SILENT. + * + * `poll' spells "do not wait" as a timeout of zero; openkal + * spells "wait without end" as a bound of zero --- timeout.h + * says so, and `kal_task_wait' established the convention + * before it. Passing the caller's zero straight through turns + * the one call that must not wait into the one that never + * returns. Measured: `poll(&pf, 1, 0)' on an idle listener + * hung, and the probe printed four lines and stopped. + * + * ⇒ The smallest bound there is, which is not zero. What it + * costs is one granularity of the environment beneath --- a + * millisecond on openkal-linux --- and what it buys is that a + * bound always means a bound. */ + const kal_u64 ns = single ? (forever ? 0 : (left ? left : OKM_NOW_NS)) + : OKM_NOW_NS; + waited = 1; + const int r = wait_in(d, ns); + if (r < 0) { if (!failure) failure = r; } + else if (r > 0) { + fds[i].revents |= POLLIN; + /* End of input on a channel or a connection is a hang-up as + * well as a readable state, and a program that reads until + * POLLHUP is the ordinary shape. A file has no hang-up. */ + if (d->ahead_eof && d->kind != OKM_STREAM) + fds[i].revents |= POLLHUP; + } + } + + /* Writability, reported without an enquiry. The head of this file + * states why there is none to make. */ + if ((fds[i].events & POLLOUT) && (d->flags & O_ACCMODE) != O_RDONLY) + fds[i].revents |= POLLOUT; + + if (fds[i].revents) ready++; + } + + if (ready) return ready; + /* ⚠️ A FAILURE IS REPORTED ONLY WHEN NOTHING WAS READY. A descriptor + * whose environment refused the enquiry must not hide the readiness of + * the others in the same set. */ + if (failure) return failure; + if (!forever && left == 0) return 0; + + /* The single descriptor was handed the caller's whole bound, so the + * wait has already happened in the environment and expiring here is the + * answer. Where nothing was waited upon --- a set of one that cannot + * receive, or one asking only about writability it was already told + * about --- the bound is still owed to the caller and is spent below. */ + if (single && waited) { + if (!forever) return 0; + continue; + } + + if (!forever) { + if (left <= OKM_ROUND_NS) left = 0; + else left -= OKM_ROUND_NS; + } + sleep_ns(OKM_ROUND_NS); + } +} diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index 2f13a40..eaa4d21 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -43,12 +43,12 @@ * which is the row that has nothing to fall back on. The same rule is already * applied to `kal_random_fill' below, and it is the second time this port has * had to learn it. */ -extern __typeof(kal_process_channel) kal_process_channel __attribute__((weak)); -extern __typeof(kal_process_channel_close) kal_process_channel_close __attribute__((weak)); +extern __typeof(kal_process_channel) kal_process_channel __attribute__((__weak__)); +extern __typeof(kal_process_channel_close) kal_process_channel_close __attribute__((__weak__)); /* Weak, for the reason given at SYS_getrandom below: the interface is * optional, and an implementation that does not provide it is absent as a * definition rather than present and refusing. */ -extern __typeof(kal_random_fill) kal_random_fill __attribute__((weak)); +extern __typeof(kal_random_fill) kal_random_fill __attribute__((__weak__)); #include #include @@ -65,6 +65,8 @@ extern __typeof(kal_random_fill) kal_random_fill __attribute__((weak)); #include #include #include +#include +#include #include #include "kstat.h" @@ -86,6 +88,14 @@ static syscall_arg_t stream_of(int fd, kal_uintptr* out) if (d->kind == OKM_STREAM || d->kind == OKM_CHANNEL || d->kind == OKM_FILE) { *out = d->stream; return 0; } + /* A CONNECTION IS A STREAM AND `openkal.net' ADDS NO TRANSFER OF ITS OWN. + * The interface says so: `kal_stream_read' and `kal_stream_write' are the + * operations that move a connection's bytes. A socket that is not connected + * has no stream, and `d->stream' is zero there. */ + if (d->kind == OKM_SOCKET) { + if (d->stream == 0) return -ENOTCONN; + *out = d->stream; return 0; + } return -EISDIR; } @@ -95,6 +105,8 @@ static syscall_arg_t do_write(int fd, const void* buf, size_t len) const syscall_arg_t r = stream_of(fd, &s); if (r) return r; if (len == 0) return 0; + 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 @@ -111,6 +123,18 @@ static syscall_arg_t do_read(int fd, void* buf, size_t len) const syscall_arg_t r = stream_of(fd, &s); if (r) return r; if (len == 0) return 0; + + struct okm_desc* d = okm_desc_of(fd); + /* ⭐ WHAT A READINESS ENQUIRY TOOK IS DELIVERED HERE, and delivering it is + * what made that enquiry's answer true rather than momentary. okm_poll.c + * states why openkal leaves no other way to answer one. A short read is a + * result every caller of `read' already handles. */ + const long held = okm_take_ahead(d, buf, len); + if (held == OKM_AHEAD_EOF) return 0; + if (held) return (syscall_arg_t)held; + + 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); @@ -384,6 +408,22 @@ int __okm_child_record(struct kal_process h) return -EAGAIN; } +/* ⚠️ A COPY OF THE CALLING IMAGE INHERITS THIS TABLE AND MUST NOT KEEP IT. + * + * The entries name programs the ORIGINAL started, and POSIX is explicit that a + * duplicate has no children. Left in place they are worse than useless: a copy + * that called `wait' would be told about a program it did not start and cannot + * wait for, and `system' inside a copy --- which waits for the child it just + * started --- could be handed one of the original's instead. + * + * Called from okm_fork.c, in the started context, before anything reads the + * table. The handles are not released: they belong to the original, which is + * still holding them. */ +void __okm_forget_children(void) +{ + for (int i = 0; i < OKM_MAX_CHILD; i++) g_child[i].used = 0; +} + static int child_index(int pid) { for (int i = 0; i < OKM_MAX_CHILD; i++) @@ -412,6 +452,32 @@ static syscall_arg_t do_wait4(int pid, int* status, int options, void* rusage) syscall_arg_t __okm_task_exit(int code); /* okm_thread.c */ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespec* t); +syscall_arg_t __okm_fork(void); /* okm_fork.c */ + +/* What kind of socket a descriptor is, asked through the same path a program + * would ask through. The two message calls need it and nothing else does, so + * the socket table is not opened up for them. */ +static int sock_type_of(int fd) +{ + int t = 0; + unsigned l = sizeof t; + if (okm_sock_getopt(fd, SOL_SOCKET, SO_TYPE, &t, &l) < 0) return -1; + return t; +} + +/* The bound `ppoll' and `pselect' state as a timespec, in the milliseconds + * `poll' states it in. A bound shorter than a millisecond is rounded UP to one + * rather than down to none: rounding down would turn a wait into a poll, and + * the interface beneath already rounds a bound up to its own granularity. */ +static int ms_of_timespec(const struct timespec* ts) +{ + if (!ts) return -1; + if (ts->tv_sec == 0 && ts->tv_nsec == 0) return 0; + long long ms = (long long)ts->tv_sec * 1000 + ts->tv_nsec / 1000000; + if (ms == 0) ms = 1; + if (ms > 0x7fffffffLL) ms = 0x7fffffffLL; + return (int)ms; +} syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, syscall_arg_t a3, syscall_arg_t a4, syscall_arg_t a5, @@ -519,6 +585,13 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * have. Refusing is the honest answer; silently ignoring it would give * a caller a byte stream where it asked for messages. */ if (flags & ~(O_CLOEXEC | O_NONBLOCK)) return -EINVAL; + /* ⚠️ AND `O_NONBLOCK' USED TO BE ACCEPTED AND CARRIED NO FURTHER. The + * flag was stored in the description and nothing read it, so a caller + * asked for a pipe that would not wait, was told it had one, and waited. + * That is the one shape the head of this file forbids. It is expressed + * now --- as the smallest bound `openkal.timeout' offers --- and refused + * where that interface is absent. */ + if ((flags & O_NONBLOCK) && !okm_can_bound()) return -ENOSYS; /* A backend that provides no `openkal.process' provides no channel, * and this is where a program learns that a pipe is not available @@ -912,9 +985,21 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case F_GETFD: return okm_fd_get_cloexec((int)a1) ? FD_CLOEXEC : 0; case F_SETFD: return okm_fd_cloexec((int)a1, ((int)a3 & FD_CLOEXEC) ? 1 : 0); case F_GETFL: return d->flags; - case F_SETFL: d->flags = (d->flags & ~(O_APPEND | O_NONBLOCK)) - | ((int)a3 & (O_APPEND | O_NONBLOCK)); - return 0; + case F_SETFL: { + const int want = (int)a3; + /* ⚠️ REFUSED WHERE IT CANNOT BE HONOURED, AND ONLY WHEN IT IS BEING + * ASKED FOR. A descriptor that was asked to be non-blocking and is + * not would make every subsequent transfer wait where the caller + * arranged not to. `O_NONBLOCK' is expressed here as the smallest + * bound `openkal.timeout' offers, so a backend that declines that + * interface cannot express it at all --- and a caller CLEARING the + * flag is asking for what such a backend always gives. */ + if ((want & O_NONBLOCK) && !(d->flags & O_NONBLOCK) && !okm_can_bound()) + return -ENOSYS; + d->flags = (d->flags & ~(O_APPEND | O_NONBLOCK)) + | (want & (O_APPEND | O_NONBLOCK)); + return 0; + } case F_SETLK: case F_SETLKW: case F_GETLK: return 0; default: return -EINVAL; } @@ -1137,6 +1222,206 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, } #endif + /* --- duplicating the calling image -------------------------------------- */ + /* ⭐ `fork' IS COMPOSED ABOVE `openkal.space' AND THE SPECIFICATION SAYS SO. + * okm_fork.c carries the composition and the header it quotes. What reaches + * here is musl's `_Fork', which issues this call with a termination signal + * and no stack; every other shape asks for a context that SHARES the + * caller's address space, which is `openkal.task' and reaches this library + * through `__clone' rather than through this seam. */ + /* ⚠️ TWO NUMBERS AND NOT ONE, AND THE SECOND IS THE ONE THAT MATTERED. + * musl's `_Fork' issues `SYS_fork' where the architecture has it and + * `SYS_clone' where it does not, so an implementation of the second alone + * is reached on aarch64 and riscv64 and never on x86_64. Measured: the + * probe reported `the calling image is duplicated (errno=38)' on the one + * architecture that has both. */ +#ifdef SYS_fork + case SYS_fork: return __okm_fork(); +#endif + case SYS_clone: { + const unsigned long flags = (unsigned long)a1; + if ((flags & ~0xffUL) != 0 || (void*)a2 != 0) return -ENOSYS; + return __okm_fork(); + } + + /* --- the network -------------------------------------------------------- */ + /* Every one of these is okm_net.c's, which holds the whole of the deferral + * that BSD's `socket' and openkal's `kal_net_connect' differ by. */ + case SYS_socket: return okm_sock_open((int)a1, (int)a2, (int)a3); + case SYS_bind: return okm_sock_bind((int)a1, (const void*)a2, (unsigned)a3); + case SYS_listen: return okm_sock_listen((int)a1, (int)a2); + case SYS_accept: return okm_sock_accept((int)a1, (void*)a2, (unsigned*)a3, 0); + case SYS_accept4: return okm_sock_accept((int)a1, (void*)a2, (unsigned*)a3, (int)a4); + case SYS_connect: return okm_sock_connect((int)a1, (const void*)a2, (unsigned)a3); + case SYS_getsockname: return okm_sock_name((int)a1, (void*)a2, (unsigned*)a3, 0); + case SYS_getpeername: return okm_sock_name((int)a1, (void*)a2, (unsigned*)a3, 1); + case SYS_shutdown: return okm_sock_shutdown((int)a1, (int)a2); + case SYS_setsockopt: + return okm_sock_setopt((int)a1, (int)a2, (int)a3, (const void*)a4, (unsigned)a5); + case SYS_getsockopt: + return okm_sock_getopt((int)a1, (int)a2, (int)a3, (void*)a4, (unsigned*)a5); + case SYS_sendto: + return okm_sock_send((int)a1, (const void*)a2, (size_t)a3, (int)a4, + (const void*)a5, (unsigned)a6); + case SYS_recvfrom: + return okm_sock_recv((int)a1, (void*)a2, (size_t)a3, (int)a4, + (void*)a5, (unsigned*)a6); + + /* The two calls that carry a vector of buffers and a place for ancillary + * data. openkal has no ancillary data --- there is no operation that passes + * a handle along a connection --- so a request carrying any is refused + * rather than performed without it. */ + case SYS_sendmsg: { + const struct msghdr* m = (const struct msghdr*)a2; + if (!m) return -EFAULT; + if (m->msg_controllen != 0) return -ENOSYS; + const int t = sock_type_of((int)a1); + if (t < 0) return -ENOTSOCK; + + if (t == SOCK_DGRAM) { + /* A MESSAGE IS SENT WHOLE OR NOT AT ALL, which openkal.datagram + * states. Gathering several buffers into one message would need a + * buffer this port has no place for, so a vector with more than one + * occupied entry is refused instead of being sent as several + * messages --- which is a different thing from what the caller + * asked for and would look like success. */ + int used = 0, idx = 0; + for (int i = 0; i < (int)m->msg_iovlen; i++) + if (m->msg_iov[i].iov_len) { used++; idx = i; } + if (used > 1) return -ENOSYS; + return okm_sock_send((int)a1, + used ? m->msg_iov[idx].iov_base : 0, + used ? m->msg_iov[idx].iov_len : 0, + (int)a3, m->msg_name, m->msg_namelen); + } + + syscall_arg_t total = 0; + for (int i = 0; i < (int)m->msg_iovlen; i++) { + if (m->msg_iov[i].iov_len == 0) continue; + const syscall_arg_t r = okm_sock_send((int)a1, m->msg_iov[i].iov_base, + m->msg_iov[i].iov_len, (int)a3, 0, 0); + if (r < 0) return total ? total : r; + total += r; + if ((size_t)r < m->msg_iov[i].iov_len) break; + } + return total; + } + case SYS_recvmsg: { + struct msghdr* m = (struct msghdr*)a2; + if (!m) return -EFAULT; + if (m->msg_controllen != 0) return -ENOSYS; + m->msg_flags = 0; + const int t = sock_type_of((int)a1); + if (t < 0) return -ENOTSOCK; + + if (t == SOCK_DGRAM) { + int used = 0, idx = 0; + for (int i = 0; i < (int)m->msg_iovlen; i++) + if (m->msg_iov[i].iov_len) { used++; idx = i; } + if (used > 1) return -ENOSYS; + return okm_sock_recv((int)a1, + used ? m->msg_iov[idx].iov_base : 0, + used ? m->msg_iov[idx].iov_len : 0, + (int)a3, m->msg_name, &m->msg_namelen); + } + + if (m->msg_name) m->msg_namelen = 0; + syscall_arg_t total = 0; + for (int i = 0; i < (int)m->msg_iovlen; i++) { + if (m->msg_iov[i].iov_len == 0) continue; + const syscall_arg_t r = okm_sock_recv((int)a1, m->msg_iov[i].iov_base, + m->msg_iov[i].iov_len, (int)a3, 0, 0); + if (r < 0) return total ? total : r; + total += r; + if ((size_t)r < m->msg_iov[i].iov_len) break; + } + return total; + } + + /* --- readiness ---------------------------------------------------------- */ + /* okm_poll.c holds the whole of what openkal permits here, and the reason + * `epoll' is still withheld: a readiness SET is a facility of one kernel, + * and asking each descriptor in turn is what an interface without one + * offers. */ +#ifdef SYS_poll + case SYS_poll: return okm_poll((void*)a1, (unsigned long)a2, (int)a3); +#endif + case SYS_ppoll: + return okm_poll((void*)a1, (unsigned long)a2, + ms_of_timespec((const struct timespec*)a3)); + + /* `select', which musl expresses as this call and this port expresses as + * `poll'. The two describe the same question in two shapes and openkal + * answers one of them. */ +#ifdef SYS_select + case SYS_select: +#endif + case SYS_pselect6: { + const int nfds = (int)a1; + fd_set* rd = (fd_set*)a2; + fd_set* wr = (fd_set*)a3; + fd_set* ex = (fd_set*)a4; + if (nfds < 0 || nfds > FD_SETSIZE) return -EINVAL; + + /* ⚠️ A BOUND ON THE SET, STATED RATHER THAN SILENT. Each descriptor in + * the set costs one bounded operation per round, and the set has to be + * held somewhere while that happens. A larger one is refused; it is not + * truncated, because a `select' that watched some of what it was given + * would report the others as never ready. */ + enum { OKM_SELECT_MAX = 128 }; + struct pollfd p[OKM_SELECT_MAX]; + int watched = 0; + for (int fd = 0; fd < nfds; fd++) { + short ev = 0; + if (rd && FD_ISSET(fd, rd)) ev |= POLLIN; + if (wr && FD_ISSET(fd, wr)) ev |= POLLOUT; + /* An exceptional condition is out-of-band data, which openkal does + * not have. A descriptor named only there can never be reported + * ready, and is therefore not watched. */ + if (!ev) continue; + if (watched == OKM_SELECT_MAX) return -EINVAL; + p[watched].fd = fd; + p[watched].events = ev; + p[watched].revents = 0; + watched++; + } + + /* ⚠️ THE TWO CALLS STATE THE BOUND IN DIFFERENT STRUCTURES, and which + * one was written is decided by the number rather than by the machine: + * `select' passes a `timeval' and `pselect6' a `timespec'. Reading one + * as the other would misread the fractional field by a factor of a + * thousand, in silence. */ + int ms; +#ifdef SYS_select + if (n == SYS_select) { + const struct timeval* tv = (const struct timeval*)a5; + struct timespec conv; + if (!tv) ms = -1; + else { + conv.tv_sec = tv->tv_sec; + conv.tv_nsec = (long)tv->tv_usec * 1000; + ms = ms_of_timespec(&conv); + } + } else +#endif + ms = ms_of_timespec((const struct timespec*)a5); + + const long r = okm_poll(p, (unsigned long)watched, ms); + if (r < 0) return r; + + if (rd) FD_ZERO(rd); + if (wr) FD_ZERO(wr); + if (ex) FD_ZERO(ex); + long count = 0; + for (int i = 0; i < watched; i++) { + if (rd && (p[i].revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL))) + { FD_SET(p[i].fd, rd); count++; } + if (wr && (p[i].revents & (POLLOUT | POLLERR | POLLNVAL))) + { FD_SET(p[i].fd, wr); count++; } + } + return count; + } + /* --- identity ---------------------------------------------------------- */ /* openkal describes a boundary between a program and its environment and * says nothing about who is running the program: an environment with no diff --git a/tools/run-probe.sh b/tools/run-probe.sh new file mode 100644 index 0000000..30bf7ee --- /dev/null +++ b/tools/run-probe.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# BUILD ONE PROBE, RUN IT UNDER A WATCHDOG, AND READ ITS OUTPUT BOTH WAYS. +# +# run-probe.sh [arguments...] +# +# with the target, where the row needs one, in `MCPP_TARGET`. +# +# ⚠️ WHY THIS IS A SCRIPT AND NOT A STEP. There are four probes now and each +# needs the same three things: a watchdog, because a program that does not +# return is as much a failure as one that returns wrongly and the job would +# otherwise spend its whole timeout finding out; a report of where a program +# that stopped was, because "exit code 139" answers nothing; and BOTH readings +# of the output, because asserting only that the program reported would pass for +# a program that printed its failures. +# +# Four copies of that would be four places for one of them to fall behind. The +# same reasoning is already written into openkal-llvm-runtime/tools. +set -euo pipefail + +dir="${1:?the example directory}" +# ⚠️ NO APOSTROPHE IN THIS MESSAGE, AND THAT IS NOT STYLE. Bash parses `${2:?...}` +# with quoting active, so "the program's name" opens a single quote that never +# closes --- and the report arrives thirty lines later as +# +# tools/run-probe.sh: line 53: syntax error near unexpected token `(' +# +# naming a line that is correct. Measured on the first run of this script in +# continuous integration, on all four rows at once: nothing local had run it, +# because the probes were being run by hand as binaries. +name="${2:?the name of the program}" +shift 2 + +cd "$dir" + +extra='' +[ -n "${MCPP_TARGET:-}" ] && extra="--target $MCPP_TARGET" +# shellcheck disable=SC2086 +mcpp build $extra + +# ⚠️ THE BINARY IS IDENTIFIED BY NAME AND BY BOTH SPELLINGS. One of the three +# systems appends a suffix, and a search for the bare name there finds nothing +# and reports it as a build that did not happen. +binary="$(find target -type f \( -name "$name" -o -name "$name.exe" \) | head -1)" +[ -n "$binary" ] || { echo "::error::$name did not build in $dir"; exit 1; } + +# Written out rather than taken from `timeout', which two of the three systems +# have and one does not. +watch() { # watch ... + local seconds="$1"; shift + "$@" & local pid=$! + ( sleep "$seconds"; kill -9 "$pid" 2> /dev/null ) & local guard=$! + wait "$pid"; local status=$? + kill "$guard" 2> /dev/null || true + return $status +} + +if watch 120 sh -c "\"$binary\" $* > run.log 2>&1"; then + cat run.log +else + status=$? + echo "--- what the program printed before it stopped (status $status) ---" + cat run.log + + # A program that stopped and a program that did not return need different + # questions asked of them. The debugger is for the first; a stack sample of + # a program that is still running is for the second, and a debugger asked to + # run a program that hangs hangs with it. + if [ "$status" -eq 137 ]; then + echo "--- it did not return; where it was ---" + "$binary" "$@" > /dev/null 2>&1 & hung=$! + sleep 5 + if command -v sample > /dev/null 2>&1; then + sample "$hung" 3 -mayDie 2>&1 | head -80 || true + elif command -v eu-stack > /dev/null 2>&1; then + eu-stack -p "$hung" 2>&1 | head -60 || true + fi + kill -9 "$hung" 2> /dev/null || true + elif command -v lldb > /dev/null 2>&1; then + watch 90 lldb --batch -o run \ + -k 'thread backtrace all' -k 'register read' -k quit \ + -- "$binary" "$@" > crash.log 2>&1 || true + cat crash.log + elif command -v gdb > /dev/null 2>&1; then + watch 90 gdb -batch -ex run -ex 'bt' --args "$binary" "$@" > crash.log 2>&1 || true + cat crash.log + fi + exit 1 +fi + +# ⚠️ BOTH DIRECTIONS. That the program reported, and that nothing it observed +# failed to hold. The first alone would pass for a program that printed its +# failures; the second alone would pass for a program that printed nothing. +grep -qE '^-- failures: 0 --$' run.log \ + || { echo "::error::$name did not report a count of failures"; exit 1; } +! grep -q '^FAIL:' run.log \ + || { echo "::error::$name reported a failure"; exit 1; } +echo " ok $name: every observation held"