From 92aeda038cdbe40bc47ea68003041e208a18b7f5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 03:51:19 +0900 Subject: [PATCH 1/9] 0.4.0 --- an image at its base: okld place, the placement note, and a loader that applies only the relocations naming a symbol (openkal 0.23) `okld place` chooses a base from the image's bytes, computes each writable segment with a file part for it (RELA's and RELR's relative relocations applied), appends them at offsets congruent to their addresses modulo 64 KiB and fills the placement note (SPEC 12.4, type 3; format 1.1). Where the implementation reports KAL_EXEC_PROP_PLACE, libokld reserves the image at that base (okld_host::reserve_at, kal_exec_alloc_at), takes the stored segments, maps each 64 KiB of PT_GNU_RELRO no symbol relocation writes from the file, and applies only the relocations naming a symbol; otherwise it relocates as before. okrun sets reserve_at (OKRUN_PLACE=0 declines it). `okld check` holds a placement to the image; `okld place` refuses a signed image. build-image.sh packs relative relocations and places every image. Over openkal 0.23.0 (openkal-linux 0.24.0, openkal-macos 0.21.0, openkal-windows 0.19.0). Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 59 ++++++- .github/workflows/release-binaries.yml | 4 +- README.md | 27 ++- bind/mcpp.toml | 2 +- docs/bind.md | 5 +- examples/build-image.sh | 9 +- examples/placed/app.c | 61 +++++++ ld/include/okld.h | 35 +++- ld/mcpp.toml | 2 +- ld/src/digest.cpp | 18 ++ ld/src/image.h | 34 ++++ ld/src/load.cpp | 91 +++++++++- ld/src/okld.cppm | 3 + ld/src/place.cpp | 233 +++++++++++++++++++++++++ ld/src/read.cpp | 68 ++++++++ run/mcpp.toml | 10 +- run/src/run.cpp | 11 ++ tools/mcpp.toml | 2 +- tools/src/okld.cpp | 42 ++++- 19 files changed, 676 insertions(+), 40 deletions(-) create mode 100644 examples/placed/app.c create mode 100644 ld/src/place.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea488e2..48ac59b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,8 @@ name: CI # an untrusted key. The okb runs through okrun and from memory; a # program of the system calls an image's library through libokrun # and through the binding openkal-bind generates, and the binding's -# refusals hold. +# refusals hold. An image at its base, anywhere and unplaced +# observes the same memory (0.4.0). on: push: @@ -74,11 +75,31 @@ jobs: # 8 MiB of the file's bytes, and a sleep: what stays resident once it is loaded. bash examples/build-image.sh "$arch" "out/$arch/resident.oke" examples/resident/app.c bash examples/build-image.sh "$arch" "out/$arch/prof.oke" examples/prof/app.c + # AT ITS BASE (0.4.0): every image here is placed by build-image.sh; + # this one is also left unplaced, its placement note empty. + bash examples/build-image.sh "$arch" "out/$arch/placed.oke" examples/placed/app.c + PLACE=0 bash examples/build-image.sh "$arch" "out/$arch/unplaced.oke" examples/placed/app.c + "$OKLD" inspect "out/$arch/placed.oke" | awk '$1 == "placement" { sub(",", "", $2); print $2 }' > "out/$arch/placed.base" + grep -q '^0x' "out/$arch/placed.base" || { echo "::error::okld place stated no base"; exit 1; } + "$OKLD" inspect "out/$arch/unplaced.oke" | grep -q '^placement *none' \ + || { echo "::error::an image left unplaced states a placement"; exit 1; } + # Placed again, the same base and the same bytes. + cp "out/$arch/placed.oke" again.oke + "$OKLD" place again.oke + cmp again.oke "out/$arch/placed.oke" || { echo "::error::placing a placed image changed it"; exit 1; } + # A changed byte of a stored segment is refused by the checker. + python3 -c "import sys; d=bytearray(open(sys.argv[1],'rb').read()); d[-1]^=1; open(sys.argv[2],'wb').write(d)" \ + "out/$arch/placed.oke" changed.oke + if "$OKLD" check changed.oke 2> changed-placement.txt; then echo "::error::a changed stored segment passed okld check"; exit 1; fi + grep -q "SPEC 12.4" changed-placement.txt done "$OKLD" keygen "$RUNNER_TEMP/seed" > out/key.hex for arch in x86_64 aarch64; do "$OKLD" sign "out/$arch/signed.oke" "$RUNNER_TEMP/seed" "$OKLD" inspect "out/$arch/signed.oke" + # The signature is over the stored segments too: placed, then signed. + if "$OKLD" place "out/$arch/signed.oke" 2> signed-place.txt; then echo "::error::a signed image was placed"; exit 1; fi + grep -q "place it, then sign it" signed-place.txt python3 -c "import sys; d=bytearray(open(sys.argv[1],'rb').read()); d[len(d)//2]^=1; open(sys.argv[2],'wb').write(d)" \ "out/$arch/signed.oke" "out/$arch/tampered.oke" done @@ -217,6 +238,26 @@ jobs: grep -q '"kal_stream_write", "count": ' "$report" || { echo "::error::the calls were not counted"; exit 1; } "$OKPROF" report prof | tee okprof.out grep -q "openkal.stream" okprof.out || { echo "::error::okprof report did not group the calls by interface"; exit 1; } + - name: At its base (0.4.0) + run: | + # Placed where the implementation places memory where it is asked + # (KAL_EXEC_PROP_PLACE): at the base `okld place' chose, its relative + # relocations already in its bytes. Anywhere (OKRUN_PLACE=0), and an + # image left unplaced: relocated. What the image prints is the same. + a='${{ matrix.arch }}' + base=$(cat "out/$a/placed.base") + "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > placed.txt + OKRUN_PLACE=0 "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > anywhere.txt + "$OKRUN" "out/$a/unplaced.oke" | tr -d '\r' > unplaced.txt + cat placed.txt anywhere.txt unplaced.txt + grep -qx "base $base" placed.txt || { echo "::error::the image was not placed at the base it states ($base)"; exit 1; } + if grep -qx "base $base" anywhere.txt; then echo "::error::OKRUN_PLACE=0 placed the image at its base"; exit 1; fi + for f in placed anywhere unplaced; do grep -v '^base ' "$f.txt" > "$f.out"; done + cmp placed.out anywhere.out && cmp placed.out unplaced.out || { echo "::error::an image at its base observes other memory"; exit 1; } + for want in "the table holds its text" "the entry is the implementation's" "written two one"; do + grep -q "placed: $want" placed.out || { echo "::error::at its base: $want"; exit 1; } + done + echo " ok an image at its base, anywhere, and unplaced observes the same memory" - name: The refusals run: | a='${{ matrix.arch }}' @@ -257,21 +298,23 @@ jobs: - name: What stays resident once an image is loaded (Linux) if: runner.os == 'Linux' run: | - # The runner releases the file it ran once the image is placed: while the - # image runs, the process maps neither the image nor the okb, and holds - # the 8 MiB segment once, as its copy. + # The runner releases its mapping of the file it ran once the image is + # placed: while the image runs, what the process maps of the image or + # the okb is the segments that are the file's pages (0.3.0), and it + # holds the 8 MiB segment once. for f in "out/${{ matrix.arch }}/resident.oke" out/resident.okb; do "$OKRUN" "$f" > resident.txt & pid=$! for i in $(seq 1 50); do grep -q loaded resident.txt 2>/dev/null && break; sleep 0.05; done grep -q "resident: loaded" resident.txt || { echo "::error::$f did not start"; exit 1; } - mapped=$(grep -c "resident\.ok" "/proc/$pid/maps" || true) + maps=$(grep "resident\.ok" "/proc/$pid/maps" || true) rss=$(awk '/^Rss:/ {print $2}' "/proc/$pid/smaps_rollup") wait "$pid" - echo " $f: mapped $mapped times, $rss KiB resident" - [ "$mapped" = 0 ] || { echo "::error::the runner still maps $f while the image runs"; exit 1; } + echo " $f: $rss KiB resident; of the file, mapped:" + echo "$maps" + if grep -q ' rw-p ' <<<"$maps"; then echo "::error::the runner still holds its mapping of $f while the image runs"; exit 1; fi [ "$rss" -lt 14336 ] || { echo "::error::$rss KiB resident for an 8 MiB image: the file's pages beside its copy"; exit 1; } done - echo " ok once loaded, the image is resident once: the file it came from is released" + echo " ok once loaded, the image is resident once: the runner's mapping of the file is released" - name: From memory, and a library from a program of this system (libokrun) run: | (cd examples/host && mcpp build --release) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index ff87f6a..5f5b8a0 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -14,7 +14,7 @@ on: description: "The release tag the binaries are built from and attached to (e.g. 0.1.3)" required: true platform: - description: "The openkal-platform whose hosted arrangement builds the tools: the one whose version group holds the implementations the tag pins (0.1.5: 0.19.0; 0.1.6: 0.19.1; 0.1.7: 0.19.2; 0.1.8: 0.20.0; 0.1.9: 0.20.1; 0.3.0: 0.21.0)" + description: "The openkal-platform whose hosted arrangement builds the tools: the one whose version group holds the implementations the tag pins (0.1.5: 0.19.0; 0.1.6: 0.19.1; 0.1.7: 0.19.2; 0.1.8: 0.20.0; 0.1.9: 0.20.1; 0.3.0: 0.21.0; 0.3.1: 0.21.1; 0.4.0: 0.23.0)" required: true default: "0.21.0" mcpp_ref: @@ -34,7 +34,7 @@ env: # the dispatch beside the tag: a tag's tools pin exact implementations, and the # platform is the one whose version group holds them (0.1.4: 0.18.0; 0.1.5: # 0.19.0, openkal-linux 0.19.0; 0.1.6: 0.19.1; 0.1.7: 0.19.2; 0.1.8: 0.20.0; 0.1.9: 0.20.1; - # 0.3.0: 0.21.0). + # 0.3.0: 0.21.0; 0.3.1: 0.21.1; 0.4.0: 0.23.0). PLATFORM_VERSION: ${{ github.event.inputs.platform }} XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' diff --git a/README.md b/README.md index 6b46e17..b8ce740 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ architecture that has a runner. |---|---|---| | `openkal-ld` | `libokld` | The format's definition. A reader of bytes, a checker, a signer (Ed25519) and a loader. Freestanding: no C library, no exceptions. Its users are libokrun, `okld`, and a C library's `dlopen`. | | `openkal-run` | `libokrun` | The runner's core. It binds an image's names to this process's one openkal implementation, in the image's calling convention, then enters the image. | -| `openkal-tools` | `okrun`, `okld`, `okprof` | `okrun [--require-signature] [--trust ] [--debugger] [--prof=[:dir]] image\|okb args...` runs an image or an okb. `okld check \| note \| sign \| keygen \| inspect \| note-source \| bundle \| bundle check` works on images and okbs on a build machine. `okprof record \| report \| diff` measures what an image did across openkal. | +| `openkal-tools` | `okrun`, `okld`, `okprof` | `okrun [--require-signature] [--trust ] [--debugger] [--prof=[:dir]] image\|okb args...` runs an image or an okb. `okld check \| note \| place \| sign \| keygen \| inspect \| note-source \| bundle \| bundle check` works on images and okbs on a build machine. `okprof record \| report \| diff` measures what an image did across openkal. | | `openkal-bind` | `mcpp.mcpplibs.openkal_bind` | A build plugin. A library published as an image (`.okso`) and a C header is called from a program of Linux, macOS or Windows through a binding generated from the header ([docs/bind.md](docs/bind.md)). | `libokld` is the loader at run time and `okld` is the tool on the build machine. They share a @@ -17,10 +17,11 @@ root and differ in role, as `ld.so` and `ld` do. ## An image ```sh -okld note-source > note.S # the two notes (SPEC 12.4) +okld note-source > note.S # the notes (SPEC 12.4) clang --target=x86_64-unknown-none-elf -ffreestanding -fPIC -femulated-tls -c app.c note.S -ld.lld -shared -z max-page-size=65536 -z now -e ok_main -o app.oke app.o note.o +ld.lld -shared -z max-page-size=65536 -z now -z pack-relative-relocs -e ok_main -o app.oke app.o note.o okld note app.oke # the interfaces its imports require +okld place app.oke # its writable segments computed for its base okld check app.oke # the convention, every clause okrun app.oke arguments... ``` @@ -62,6 +63,18 @@ starts a program with the system's null device at a position (`KAL_SPAWN_NULL_IN `_ERR`), which the image's C library maps `/dev/null` onto; on Windows an image's workers started with their output discarded could not start before. +**An image at its base** (0.4.0, openkal 0.23): `okld place` chooses a base from the image's +bytes (a multiple of 4 GiB from 16 TiB), computes each writable segment for it, every relative +relocation applied, and appends the result to the file with a placement note (SPEC 12.4, type 3). +Where the implementation reports `KAL_EXEC_PROP_PLACE`, the loader reserves the image there +(`kal_exec_alloc_at`), takes the stored segments and applies only the relocations that name a +symbol; each 64 KiB of the read-only-after-relocation data that none of those writes is the +file's pages. Where the base is taken, or the image states none, the loader places it anywhere +and relocates it, as before; `OKRUN_PLACE=0` asks for that. The read-only data of all the +processes of one image are then the same pages, as those of a program linked for one address +are: a start of clangd as an image made 606 page faults at its base, 1 047 anywhere. An image +is placed before it is signed, since the signature covers the stored bytes. + ## A measurement of an image The runner binds every name an image calls across openkal and read the image's file, so it @@ -115,8 +128,12 @@ its header and calls it, on openkal and on every other system alike ([docs/bind. and an unsigned manifest under `--require-signature` are refused. - A program of each system calls an image's library through `okrun_open_library`, and through the binding `openkal-bind` generates (including a callback); the boundary's refusals hold. -- Once an image is loaded, the runner maps neither it nor the okb it came from, and holds its - segments once (Linux, from the process's mappings). +- Once an image is loaded, the runner holds no mapping of its own of the image or the okb it + came from, only the segments that are the file's pages, and holds its segments once (Linux, + from the process's mappings). +- An image at its base, the same image placed anywhere (`OKRUN_PLACE=0`) and the image left + unplaced observe the same memory; placing a placed image changes no byte; a changed byte of + a stored segment is refused by `okld check`, and a signed image is not placed. - An okb states its images' chunk digests, which okrun verifies on several contexts and `okld bundle check` beside the whole digest; a changed byte is refused by either. - libokld's SHA-256, CRC-32, ZIP reader and writer (with ZIP64) and manifest reader hold diff --git a/bind/mcpp.toml b/bind/mcpp.toml index ef14570..372be7c 100644 --- a/bind/mcpp.toml +++ b/bind/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-bind" -version = "0.3.2" +version = "0.4.0" # `mcpp::xpkg_request', `mcpp::decision' (protocol 15). mcpp = ">=2026.10.1.3" description = "A build plugin: a library published as an ok image (.okso) and a C header is called from a program of any system, through a binding generated from the header and libokrun." diff --git a/docs/bind.md b/docs/bind.md index 027a913..5b05b17 100644 --- a/docs/bind.md +++ b/docs/bind.md @@ -24,10 +24,10 @@ sources = [] include_dirs = ["include"] [target.'cfg(not(os = "openkal"))'.dependencies] -openkal-run = "0.3.2" +openkal-run = "0.4.0" [build-dependencies] -openkal-bind = "0.3.2" +openkal-bind = "0.4.0" ``` ```cpp @@ -108,4 +108,5 @@ llvm toolchain's clang converts the call. | 0.3.0 | No change of its own; released with the archive, whose loader maps an image's instructions and read-only data from its file where the implementation can (openkal 0.21, `kal_exec_map_part`). | | 0.3.1 | No change of its own; released with the archive, whose runner admits a remembered okb member by its shape without reading it. | | 0.3.2 | No change of its own; released with the archive, whose runner is built over openkal 0.22's implementations: a started program is given the system's null device at a position (`KAL_SPAWN_NULL_*`). | +| 0.4.0 | No change of its own; released with the archive, whose loader places an image at the base `okld place` chose for it (openkal 0.23, `kal_exec_alloc_at`) and then applies only the relocations that name a symbol. | | 0.1.9 | No change of its own; released with the archive, whose runner on macOS is built over openkal-macos 0.17.1 (`kal_task_current` without a system call). | diff --git a/examples/build-image.sh b/examples/build-image.sh index 1ba9b4f..cea530e 100755 --- a/examples/build-image.sh +++ b/examples/build-image.sh @@ -3,8 +3,10 @@ # (or, named *.okso, an image another needs) from freestanding C, # with the compiler and linker any LLVM carries (SPEC 12.2): no operating # system in the triple, position-independent, emulated thread-local storage, -# x18 left alone on AArch64, segments aligned to 64 KiB, the two notes linked -# in, and the requirements written by `okld note'. +# x18 left alone on AArch64, segments aligned to 64 KiB, relative relocations +# packed, the notes linked in, the requirements written by `okld note', and the +# image placed at its base by `okld place' (openkal 0.23; PLACE=0 leaves the +# placement note empty). set -euo pipefail arch="$1"; out="$2"; shift 2 sources=(); link=() @@ -27,9 +29,10 @@ for src in "${sources[@]}" "$work/note.S"; do done entry=(-e ok_main) case "$out" in *.okso) entry=(-soname "$(basename "$out")") ;; esac -"$LLD" -shared -z max-page-size=65536 -z now -z noexecstack --hash-style=both "${entry[@]}" \ +"$LLD" -shared -z max-page-size=65536 -z now -z noexecstack -z pack-relative-relocs --hash-style=both "${entry[@]}" \ -o "$out" "${objs[@]}" "${link[@]}" "$OKLD" note "$out" +[ "${PLACE:-1}" = 0 ] || "$OKLD" place "$out" with=() for l in "${link[@]}"; do case "$l" in *.okso) with+=(--with "$l") ;; esac; done "$OKLD" check "$out" "${with[@]}" diff --git a/examples/placed/app.c b/examples/placed/app.c new file mode 100644 index 0000000..26f4f84 --- /dev/null +++ b/examples/placed/app.c @@ -0,0 +1,61 @@ +/* An image at its base (openkal 0.23, SPEC 12.3 and 12.4, type 3). + * + * Its read-only-after-relocation data holds addresses: of its own text, which + * relative relocations compute (a table of 128 KiB, so that whole 64 KiB blocks + * of it are the file's pages where the implementation maps files), and of an + * implementation's entry, which a relocation naming a symbol computes. Its + * writable data holds addresses and is written. What it prints is the same + * wherever it is placed; the base it reports is the one `okld inspect' states + * when okrun places it there. */ +typedef unsigned long kal_uintptr; +typedef long kal_intptr; +struct kal_stream { kal_uintptr h; }; + +struct kal_stream kal_stdout(void); +kal_intptr kal_stream_write(struct kal_stream, const void*, kal_uintptr); + +void* memcpy(void* d, const void* s, unsigned long n) { + unsigned char* a = d; const unsigned char* b = s; + while (n--) *a++ = *b++; + return d; +} +void* memset(void* d, int c, unsigned long n) { + unsigned char* a = d; + while (n--) *a++ = (unsigned char)c; + return d; +} + +extern const char __ehdr_start[] __attribute__((visibility("hidden"))); + +static const char text[] = "placed"; +static const char* const many[16384] = { [0 ... 16383] = text }; +static kal_intptr (*const write_to)(struct kal_stream, const void*, kal_uintptr) = kal_stream_write; +static const char* chosen[2] = { "one", "two" }; + +static unsigned long len(const char* s) { unsigned long n = 0; while (s[n]) ++n; return n; } +static void put(const char* s) { write_to(kal_stdout(), s, len(s)); } +static void put_hex(kal_uintptr v) { + char b[19]; int i = 18; b[i] = 0; + do { b[--i] = "0123456789abcdef"[v & 15]; v >>= 4; } while (v); + b[--i] = 'x'; b[--i] = '0'; + put(b + i); +} + +int ok_main(void) { + unsigned long same = 0; + for (unsigned long i = 0; i < sizeof many / sizeof many[0]; ++i) + if (many[i] == text) ++same; + put(same == sizeof many / sizeof many[0] ? "placed: the table holds its text\n" : "placed: THE TABLE IS WRONG\n"); + put(write_to == kal_stream_write ? "placed: the entry is the implementation's\n" : "placed: THE ENTRY IS WRONG\n"); + const char* was = chosen[0]; + chosen[0] = chosen[1]; + chosen[1] = was; + put("placed: written "); + put(chosen[0]); + put(" "); + put(chosen[1]); + put("\nbase "); + put_hex((kal_uintptr)__ehdr_start); + put("\n"); + return 0; +} diff --git a/ld/include/okld.h b/ld/include/okld.h index 3a591ff..f4e7944 100644 --- a/ld/include/okld.h +++ b/ld/include/okld.h @@ -19,7 +19,7 @@ typedef __SIZE_TYPE__ okld_size; /* The version of clause 12 this library reads and writes. An image of a * higher major version is refused. */ #define OKLD_FORMAT_MAJOR 1u -#define OKLD_FORMAT_MINOR 0u +#define OKLD_FORMAT_MINOR 1u /* What a reading reports about an image. Every field is filled by * `okld_read'; nothing is mapped. */ @@ -259,10 +259,43 @@ struct okld_host { * placed, so that gdb and lldb show its symbols and its source. */ int debugger; void* ctx; + /* Reserves the region at `at' (kal_exec_alloc_at, openkal 0.23): `at', or + * null with nothing reserved, as it is where the implementation does not + * report KAL_EXEC_PROP_PLACE. An image that states a placement (SPEC 12.4, + * type 3) is placed at its base when this answers, and its relative + * relocations are not applied; null, or refused, and the image is reserved + * anywhere and relocated. Version 0.4. */ + void* (*reserve_at)(void* at, okld_size bytes, void* ctx); }; struct okld_module; +/* --- placement (SPEC 12.4, type 3; openkal 0.23) ---------------------------- */ + +/* The entries the placement note a sealed image carries has room for. */ +#define OKLD_PLACEMENT_MAX 8u + +/* The base `okld_place' chooses for an image when it is given none: from the + * SHA-256 of its bytes, a multiple of 4 GiB from 16 TiB to below 32 TiB, where + * no system places a program's own memory and each image of a process is likely + * to have its own. */ +okld_u64 okld_place_base(const void* image, okld_size size); + +/* The image placed at `base' (0: okld_place_base): its bytes, each writable + * loadable segment computed for `base' --- its file bytes with every relative + * relocation, RELA's and RELR's, applied --- appended at a file offset congruent + * to its address modulo 65536, and the placement note filled. A placement the + * image carries already is replaced. Writes into `out' when `cap' holds it and + * returns the size, as okld_zip_write does; 0 with `why' filled when the image + * has no placement note to fill, more writable segments than the note has room + * for, or a base that is not a multiple of 65536. */ +okld_u64 okld_place(const void* image, okld_size size, okld_u64 base, void* out, okld_u64 cap, + struct okld_why* why); + +/* What the placement note of an image states: its base and the number of its + * entries (0 when it states none). */ +int okld_placement(const void* image, okld_size size, okld_u64* base, okld_u32* count); + /* Maps, relocates and publishes an image and, first, every image it needs. * Initialisers are not run: `okld_initialise' runs them, so that a caller can * do what it must between. The loader keeps no pointer into `bytes', nor into diff --git a/ld/mcpp.toml b/ld/mcpp.toml index 9b89d5b..3f81510 100644 --- a/ld/mcpp.toml +++ b/ld/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-ld" -version = "0.3.2" +version = "0.4.0" description = "libokld: the ok format (openkal image format) --- its definition, a reader of bytes, a checker, a signer, and a loader. Freestanding: no C library, no exceptions." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/ld/src/digest.cpp b/ld/src/digest.cpp index 8e5f525..f62f092 100644 --- a/ld/src/digest.cpp +++ b/ld/src/digest.cpp @@ -213,6 +213,24 @@ extern "C" void okld_sha256(const void* bytes, okld_size n, unsigned char out[32 s.final(out); } +namespace okld { +void sha256_except(const void* bytes, size n, size hole, size hole_n, unsigned char out[32]) { + const auto* b = static_cast(bytes); + if (hole > n) hole = n; + if (hole_n > n - hole) hole_n = n - hole; + sha256 s; + s.update(b, hole); + static constexpr u8 zero[64] = {}; + for (size left = hole_n; left > 0;) { + const size take = left < 64 ? left : 64; + s.update(zero, take); + left -= take; + } + s.update(b + hole + hole_n, n - hole - hole_n); + s.final(out); +} +} // namespace okld + extern "C" okld_u32 okld_crc32(okld_u32 crc, const void* bytes, okld_size n) { const auto* p = static_cast(bytes); u32 c = ~crc; diff --git a/ld/src/image.h b/ld/src/image.h index 9d65df6..d8cdcda 100644 --- a/ld/src/image.h +++ b/ld/src/image.h @@ -68,4 +68,38 @@ bool open(const void* bytes, size n, view& v, char* why, size why_cap); // The descriptor of an OPENKAL note of a type, writable when the bytes are. unsigned char* find_note(const view& v, unsigned char* writable, elf::u32 type, elf::u32* descsz); +// THE PLACEMENT NOTE (SPEC 12.4, type 3; openkal 0.23): a base, and for each +// writable loadable segment where its bytes computed for that base lie in the +// file. Its descriptor: the base, the count and a zero word, then the entries. +struct placement { + struct entry { elf::u64 vaddr = 0, offset = 0, size = 0; }; + elf::u64 base = 0; + elf::u32 count = 0; + entry e[8]; +}; +constexpr elf::u32 placement_note = 3; +constexpr size placement_descriptor = 16 + 8 * sizeof(placement::entry); + +// The placement an image states, read and held to the image: one entry for +// each writable loadable segment with bytes in the file, in ascending order of +// address, its address and its size in the file, its bytes within the file at +// an offset congruent to its address modulo 65536, and a base that is a +// multiple of 65536. False when it states none --- its requirements note states +// a version below 1.1, or its count is zero --- or when what it states does not +// describe the image. +bool read_placement(const view& v, placement& out); + +// The relative relocations, RELA's and RELR's, that write the segment at +// `vaddr' of `filesz' bytes, applied for `base' to `seg', which holds the +// segment's bytes as they are in the file. The relocations that name a symbol +// are not applied. +void apply_relative(const view& v, elf::u64 base, unsigned char* seg, elf::u64 vaddr, elf::u64 filesz); + +// SHA-256 of `n' bytes with the `hole_n' from `hole' read as zero. +void sha256_except(const void* bytes, size n, size hole, size hole_n, unsigned char out[32]); + +// Whether a relocation names a symbol (it is not a relative one), for the +// architecture of the image. +bool names_a_symbol(const view& v, elf::u32 type); + } // namespace okld diff --git a/ld/src/load.cpp b/ld/src/load.cpp index e68bba5..1c997aa 100644 --- a/ld/src/load.cpp +++ b/ld/src/load.cpp @@ -202,7 +202,7 @@ void withdraw(okld_module* m) { // segment that is not writable may be the file's own pages (okld_host::map), // and a write the format does not allow (SPEC 12.2: no text relocation) is // refused rather than made wherever its offset points. -int relocate(okld_module* m, const view& v, const okld_host* host, const writable& w, okld_why* why) { +int relocate(okld_module* m, const view& v, const okld_host* host, const writable& w, bool placed, okld_why* why) { const elf::u64 b = reinterpret_cast(m->base); const bool x86 = v.eh->machine == elf::em_x86_64; auto symbol_address = [&](elf::u32 index, bool& ok) -> elf::u64 { @@ -231,6 +231,9 @@ int relocate(okld_module* m, const view& v, const okld_host* host, const writabl } auto* where = reinterpret_cast(m->base + r[k].offset); const elf::u32 t = elf::r_type(r[k].info); + // AT ITS BASE THE RELATIVE ONES ARE ALREADY IN THE BYTES (SPEC + // 12.3, openkal 0.23): what remains names a symbol. + if (placed && !names_a_symbol(v, t)) continue; bool ok = true; if (x86) { switch (t) { @@ -257,8 +260,9 @@ int relocate(okld_module* m, const view& v, const okld_host* host, const writabl return true; }; if (!apply(elf::dt_rela, elf::dt_relasz) || !apply(elf::dt_jmprel, elf::dt_pltrelsz)) return -1; - // DT_RELR: packed relative relocations. - if (const elf::u64 at = v.dynamic(elf::dt_relr, ~0ull); at != ~0ull) { + // DT_RELR: packed relative relocations, already in the bytes of an image + // at its base. + if (const elf::u64 at = v.dynamic(elf::dt_relr, ~0ull); !placed && at != ~0ull) { const elf::u64 bytes = v.dynamic(elf::dt_relrsz); const auto* e = reinterpret_cast(m->base + at); elf::u64 next = 0; // the offset the first bit of a bitmap entry names @@ -298,6 +302,64 @@ void discard(okld_module* m) { } } // namespace +namespace { +// A WRITABLE SEGMENT OF AN IMAGE AT ITS BASE: the bytes the image stores for +// that base (SPEC 12.4, type 3). Each 64 KiB of it that lies wholly within the +// read-only-after-relocation range, and that no relocation naming a symbol +// writes, is made the file's pages where the host can (kal_exec_map_part): it +// is never written again, as the same data of a program linked for one address +// is not. The rest is copied, and the size in memory beyond the file's is zero. +void place_segment(okld_module* m, const view& v, const okld_host* host, const elf::phdr& p, + const placement& at, elf::u64 relro_from, elf::u64 relro_to, const void* bytes) { + const placement::entry* e = nullptr; + for (elf::u32 i = 0; i < at.count; ++i) if (at.e[i].vaddr == p.vaddr) e = &at.e[i]; + const auto* stored = static_cast(bytes) + e->offset; + // The 64 KiB blocks a relocation naming a symbol writes, which are copied. + constexpr elf::u64 block = file_alignment; + const elf::u64 first = round_down(p.vaddr, block), blocks = (round_up(p.vaddr + p.filesz, block) - first) / block; + bool named[512] = {}; + const bool mappable = host->map && blocks <= 512 && relro_to > relro_from; + if (mappable) { + auto mark = [&](elf::i64 tag, elf::i64 size_tag) { + const auto at_ = v.offset_of(v.dynamic(tag, ~0ull)); + const auto n = v.dynamic(size_tag); + const auto* r = at_ == ~0ull ? nullptr : v.at(at_, n / sizeof(elf::rela)); + for (size k = 0; r && k < n / sizeof(elf::rela); ++k) { + if (!names_a_symbol(v, elf::r_type(r[k].info))) continue; + const elf::u64 o = r[k].offset; + if (o + 8 <= p.vaddr || o >= p.vaddr + p.filesz) continue; + named[(o > first ? o - first : 0) / block] = true; + const elf::u64 last = (o + 7 - first) / block; + if (last < blocks) named[last] = true; + } + }; + mark(elf::dt_rela, elf::dt_relasz); + mark(elf::dt_jmprel, elf::dt_pltrelsz); + } + for (elf::u64 b = 0; b < blocks; ++b) { + const elf::u64 lo = first + b * block, hi = lo + block; + const elf::u64 from = lo > p.vaddr ? lo : p.vaddr; + const elf::u64 to = hi < p.vaddr + p.filesz ? hi : p.vaddr + p.filesz; + // A run of blocks the file can hold, mapped together. + if (mappable && lo >= p.vaddr && hi <= p.vaddr + p.filesz && lo >= relro_from && hi <= relro_to && !named[b]) { + elf::u64 end = b + 1; + while (end < blocks) { + const elf::u64 elo = first + end * block, ehi = elo + block; + if (ehi > p.vaddr + p.filesz || ehi > relro_to || named[end]) break; + ++end; + } + const elf::u64 bytes_ = (end - b) * block; + if (host->map(m->base, lo, bytes_, bytes, e->offset + (lo - p.vaddr), 0, host->ctx) == 0) { + b = end - 1; + continue; + } + } + copy(m->base + from, stored + (from - p.vaddr), to - from); + } + fill(m->base + p.vaddr + p.filesz, 0, p.memsz - p.filesz); +} +} // namespace + extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const okld_host* host, okld_module** out, okld_why* why) { view v; @@ -342,7 +404,17 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const for (unsigned k = 0; k < v.eh->phnum; ++k) if (v.ph[k].type == elf::pt_load && v.ph[k].vaddr + v.ph[k].memsz > m->span) m->span = v.ph[k].vaddr + v.ph[k].memsz; m->span = round_up(m->span, g); - m->base = static_cast(host->reserve(m->span, host->ctx)); + // AT ITS BASE WHERE THE IMAGE STATES ONE AND THE HOST CAN PLACE IT THERE + // (SPEC 12.3, openkal 0.23): its writable segments are then the bytes it + // stores for that base, and no relative relocation is applied. Otherwise + // anywhere, and every relocation is. + placement placed_at; + bool placed = false; + if (host->reserve_at && read_placement(v, placed_at) && placed_at.base % g == 0) { + m->base = static_cast(host->reserve_at(reinterpret_cast(placed_at.base), m->span, host->ctx)); + placed = m->base != nullptr; + } + if (!placed) m->base = static_cast(host->reserve(m->span, host->ctx)); if (!m->base) { say(why, "the address space for the image could not be reserved"); host->free(m, sizeof *m, host->ctx); return -1; } // Each segment is its file bytes and then zeros to its size in memory. What // lies between segments is no segment's and is not written: a page the image @@ -358,6 +430,11 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const // shares; otherwise, and where the host declines, copied. bool mapped[32] = {}; writable w; + // The read-only-after-relocation range, whose pages an image at its base + // need not write: those no symbol relocation writes are the file's. + elf::u64 relro_from = 0, relro_to = 0; + for (unsigned k = 0; k < v.eh->phnum; ++k) + if (v.ph[k].type == elf::pt_gnu_relro) { relro_from = v.ph[k].vaddr; relro_to = v.ph[k].vaddr + v.ph[k].memsz; } for (unsigned k = 0; k < v.eh->phnum; ++k) { const auto& p = v.ph[k]; if (p.type != elf::pt_load) continue; @@ -366,6 +443,10 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const if (p.flags & elf::pf_w) { if (w.count == 8) { say(why, "the image has more than eight writable segments"); host->release(m->base, m->span, host->ctx); host->free(m, sizeof *m, host->ctx); return -1; } w.from[w.count] = p.vaddr; w.to[w.count] = p.vaddr + p.memsz; ++w.count; + if (placed && p.filesz != 0) { + place_segment(m, v, host, p, placed_at, relro_from, relro_to, bytes); + continue; + } } else if (host->map && k < 32 && p.filesz == p.memsz && p.filesz != 0 && p.vaddr % file_alignment == p.offset % file_alignment) { const elf::u64 lo = round_down(p.vaddr, file_alignment), hi = round_up(p.vaddr + p.filesz, g); @@ -409,7 +490,7 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const m->init_array = v.dynamic(elf::dt_init_array); m->init_arraysz = v.dynamic(elf::dt_init_arraysz); m->fini_array = v.dynamic(elf::dt_fini_array); m->fini_arraysz = v.dynamic(elf::dt_fini_arraysz); - if (relocate(m, v, host, w, why) != 0) { discard(m); return -1; } + if (relocate(m, v, host, w, placed, why) != 0) { discard(m); return -1; } // Publish what executes and was copied; the rest stays writable. A segment // that is the file's pages is executable already, and its bytes reached diff --git a/ld/src/okld.cppm b/ld/src/okld.cppm index bd99490..304b3a7 100644 --- a/ld/src/okld.cppm +++ b/ld/src/okld.cppm @@ -34,6 +34,9 @@ export using ::okld_name; export using ::okld_unload; export using ::okld_iterate; export using ::okld_containing; +export using ::okld_place_base; +export using ::okld_place; +export using ::okld_placement; export using ::okld_sign_message; export using ::okld_verify_message; export using ::okld_sha256; diff --git a/ld/src/place.cpp b/ld/src/place.cpp new file mode 100644 index 0000000..f9c97ff --- /dev/null +++ b/ld/src/place.cpp @@ -0,0 +1,233 @@ +// Placing an image at its base (SPEC 12.4, type 3; 12.3; openkal 0.23). +// +// An image is position-independent (12.2), so a loader that places it anywhere +// writes, on every start, each address its relative relocations compute: for a +// language server's image, 164 000 of them over the two mebibytes of its +// read-only-after-relocation data, made resident in every one of the 176 +// programs a build of its modules starts. A program of the environment linked +// for one address writes none of them, and its read-only data is the file's +// pages. `okld place' computes the writable segments once, for one base chosen +// from the image's bytes, and appends them to the file; a loader given that +// base takes them and applies only the relocations that name a symbol. +#include +#include "image.h" + +namespace okld { +namespace { + +constexpr elf::u64 alignment = 65536; + +// The end of what the ELF describes: segments, program headers and sections. +// What lies beyond it is a placement's stored segments (or nothing). +elf::u64 content_end(const view& v) { + elf::u64 end = v.eh->phoff + static_cast(v.eh->phnum) * sizeof(elf::phdr); + for (unsigned k = 0; k < v.eh->phnum; ++k) + if (v.ph[k].offset + v.ph[k].filesz > end) end = v.ph[k].offset + v.ph[k].filesz; + if (v.eh->shoff) { + const elf::u64 sh = v.eh->shoff + static_cast(v.eh->shnum) * v.eh->shentsize; + if (sh > end) end = sh; + } + return end < v.len ? end : v.len; +} + +// A writable loadable segment with bytes in the file: one a placement stores. +// One that is only zeros (`.bss' alone) has nothing to store. +bool stored(const elf::phdr& p) { return p.type == elf::pt_load && (p.flags & elf::pf_w) && p.filesz != 0; } + +const elf::phdr* writable_segment(const view& v, elf::u64 vaddr) { + for (unsigned k = 0; k < v.eh->phnum; ++k) + if (stored(v.ph[k]) && v.ph[k].vaddr == vaddr) return &v.ph[k]; + return nullptr; +} + +unsigned writable_count(const view& v) { + unsigned n = 0; + for (unsigned k = 0; k < v.eh->phnum; ++k) + if (stored(v.ph[k])) ++n; + return n; +} + +// The requirements note states version 1.1 of clause 12.4 or a later one: a +// loader reads a placement only then (12.4). +bool states_placement(const view& v) { + elf::u32 sz = 0; + const unsigned char* d = find_note(v, nullptr, 1, &sz); + if (!d || sz < 8) return false; + elf::u32 major = 0, minor = 0; + copy(&major, d, 4); + copy(&minor, d + 4, 4); + return major > 1 || (major == 1 && minor >= 1); +} + +} // namespace + +bool names_a_symbol(const view& v, elf::u32 type) { + return v.eh->machine == elf::em_x86_64 + ? type != elf::r_x86_64_none && type != elf::r_x86_64_relative + : type != elf::r_aarch64_none && type != elf::r_aarch64_relative; +} + +bool read_placement(const view& v, placement& out) { + if (!states_placement(v)) return false; + elf::u32 sz = 0; + const unsigned char* d = find_note(v, nullptr, placement_note, &sz); + if (!d || sz < 16) return false; + copy(&out.base, d, 8); + copy(&out.count, d + 8, 4); + if (out.count == 0 || out.count > 8 || sz < 16 + out.count * sizeof(placement::entry)) return false; + if (out.base == 0 || out.base % alignment != 0 || out.count != writable_count(v)) return false; + for (elf::u32 i = 0; i < out.count; ++i) { + copy(&out.e[i], d + 16 + i * sizeof(placement::entry), sizeof(placement::entry)); + const auto& e = out.e[i]; + if (i > 0 && e.vaddr <= out.e[i - 1].vaddr) return false; + const elf::phdr* p = writable_segment(v, e.vaddr); + if (!p || p->filesz != e.size) return false; + if (e.offset % alignment != e.vaddr % alignment) return false; + if (e.offset > v.len || e.size > v.len - e.offset) return false; + } + return true; +} + +void apply_relative(const view& v, elf::u64 base, unsigned char* seg, elf::u64 vaddr, elf::u64 filesz) { + const bool x86 = v.eh->machine == elf::em_x86_64; + const elf::u32 relative = x86 ? elf::r_x86_64_relative : elf::r_aarch64_relative; + auto inside = [&](elf::u64 at) { return at >= vaddr && at <= vaddr + filesz && filesz - (at - vaddr) >= 8; }; + auto table = [&](elf::i64 tag, elf::i64 size_tag) { + const auto at = v.offset_of(v.dynamic(tag, ~0ull)); + const auto bytes = v.dynamic(size_tag); + if (at == ~0ull || bytes == 0) return; + const auto* r = v.at(at, bytes / sizeof(elf::rela)); + if (!r) return; + for (size k = 0; k < bytes / sizeof(elf::rela); ++k) { + if (elf::r_type(r[k].info) != relative || !inside(r[k].offset)) continue; + const elf::u64 value = base + static_cast(r[k].addend); + copy(seg + (r[k].offset - vaddr), &value, 8); + } + }; + table(elf::dt_rela, elf::dt_relasz); + table(elf::dt_jmprel, elf::dt_pltrelsz); + const auto relr = v.offset_of(v.dynamic(elf::dt_relr, ~0ull)); + const auto relr_bytes = v.dynamic(elf::dt_relrsz); + if (relr == ~0ull || relr_bytes == 0) return; + const auto* e = v.at(relr, relr_bytes / 8); + if (!e) return; + auto add = [&](elf::u64 at) { + if (!inside(at)) return; + elf::u64 value = 0; + copy(&value, seg + (at - vaddr), 8); + value += base; + copy(seg + (at - vaddr), &value, 8); + }; + elf::u64 next = 0; + for (size k = 0; k < relr_bytes / 8; ++k) { + if ((e[k] & 1) == 0) { + add(e[k]); + next = e[k] + 8; + } else { + elf::u64 bits = e[k] >> 1; + for (elf::u64 i = 0; bits; ++i, bits >>= 1) + if (bits & 1) add(next + 8 * i); + next += 63 * 8; + } + } +} + +} // namespace okld + +using namespace okld; + +// The bytes the ELF describes with the placement read as zero: the same +// before an image is placed and after, and however often it is. +extern "C" okld_u64 okld_place_base(const void* image, okld_size size) { + unsigned char d[32]; + view v; + char why[8]; + elf::u32 sz = 0; + const unsigned char* note = open(image, size, v, why, sizeof why) ? find_note(v, nullptr, placement_note, &sz) : nullptr; + const okld_size end = note ? content_end(v) : size; + const okld_size hole = note ? static_cast(note - static_cast(image)) : 0; + sha256_except(image, end, hole, note ? sz : 0, d); + const okld_u64 slot = (static_cast(d[0]) | static_cast(d[1]) << 8) & 0xfff; + return (okld_u64{1} << 44) + slot * (okld_u64{1} << 32); +} + +extern "C" okld_u64 okld_place(const void* image, okld_size n, okld_u64 base, void* out, okld_u64 cap, + okld_why* asked) { + okld_why own{}; + okld_why* why = asked ? asked : &own; + view v; + if (!open(image, n, v, why->text, sizeof why->text)) return 0; + elf::u32 sz = 0; + if (!find_note(v, nullptr, placement_note, &sz) || sz < placement_descriptor) { + sentence(why->text, sizeof why->text) + << "no placement note to write into (SPEC 12.4, type 3): link the note object `okld note-source' writes"; + return 0; + } + // The signature is over every byte of the file (12.4, type 2), the stored + // segments too: an image is placed, and then signed. + elf::u32 ssz = 0; + if (const unsigned char* sig = find_note(v, nullptr, 2, &ssz)) { + for (elf::u32 i = 0; i < ssz; ++i) + if (sig[i]) { + sentence(why->text, sizeof why->text) << "the image is signed, and its signature is over the bytes a placement adds: place it, then sign it"; + return 0; + } + } + const unsigned count = writable_count(v); + if (count == 0 || count > 8) { + sentence(why->text, sizeof why->text) << "the image has no writable segment, or more than eight"; + return 0; + } + const elf::u64 end = content_end(v); + if (base == 0) base = okld_place_base(image, n); + if (base % alignment != 0) { + sentence(why->text, sizeof why->text) << "a base that is not a multiple of 65536"; + return 0; + } + // Each writable segment after what the ELF describes, at an offset + // congruent to its address: a loader maps its pages as it maps the file's. + placement p; + p.base = base; + elf::u64 at = end; + for (unsigned k = 0; k < v.eh->phnum; ++k) { + const auto& ph = v.ph[k]; + if (!stored(ph)) continue; + const elf::u64 offset = at + ((ph.vaddr % alignment) + alignment - (at % alignment)) % alignment; + p.e[p.count++] = { ph.vaddr, offset, ph.filesz }; + at = offset + ph.filesz; + } + const elf::u64 total = at; + if (!out || cap < total) return total; + + auto* o = static_cast(out); + copy(o, image, end); + fill(o + end, 0, total - end); + for (elf::u32 i = 0; i < p.count; ++i) { + const elf::phdr* ph = writable_segment(v, p.e[i].vaddr); + copy(o + p.e[i].offset, static_cast(image) + ph->offset, ph->filesz); + apply_relative(v, base, o + p.e[i].offset, ph->vaddr, ph->filesz); + } + view w; + char ignored[8]; + if (!open(o, total, w, ignored, sizeof ignored)) return 0; + unsigned char* d = find_note(w, o, placement_note, &sz); + fill(d, 0, sz); + copy(d, &p.base, 8); + copy(d + 8, &p.count, 4); + for (elf::u32 i = 0; i < p.count; ++i) copy(d + 16 + i * sizeof(placement::entry), &p.e[i], sizeof(placement::entry)); + return total; +} + +extern "C" int okld_placement(const void* image, okld_size size, okld_u64* base, okld_u32* count) { + view v; + char why[8]; + placement p; + if (!open(image, size, v, why, sizeof why) || !read_placement(v, p)) { + if (base) *base = 0; + if (count) *count = 0; + return 0; + } + if (base) *base = p.base; + if (count) *count = p.count; + return 1; +} diff --git a/ld/src/read.cpp b/ld/src/read.cpp index 4fd11ab..7f1f2e3 100644 --- a/ld/src/read.cpp +++ b/ld/src/read.cpp @@ -161,6 +161,60 @@ struct checker { okld_report report; void* ctx; int failures = 0; void fail(const char* s) { ++failures; if (report) report(s, ctx); } }; + +// Each stored segment against the file: the words a relative relocation +// writes hold what it computes for the base, and every other word is the +// file's. Counted rather than copied, since nothing here allocates. +void verify_placement(const view& v, const placement& p, checker& c) { + const bool x86 = v.eh->machine == elf::em_x86_64; + const elf::u32 relative = x86 ? elf::r_x86_64_relative : elf::r_aarch64_relative; + for (elf::u32 i = 0; i < p.count; ++i) { + const auto& e = p.e[i]; + const elf::u64 file = v.offset_of(e.vaddr); + if (file == ~0ull) { c.fail("a placed segment has no bytes in the file"); return; } + const unsigned char* was = v.data + file; + const unsigned char* now = v.data + e.offset; + auto word = [](const unsigned char* b, elf::u64 at) { elf::u64 w = 0; copy(&w, b + at, 8); return w; }; + auto inside = [&](elf::u64 at) { return at >= e.vaddr && at <= e.vaddr + e.size && e.size - (at - e.vaddr) >= 8; }; + elf::u64 written = 0, wrong = 0; + auto expect = [&](elf::u64 at, elf::u64 value) { + if (!inside(at)) return; + if (word(was, at - e.vaddr) != value) ++written; + if (word(now, at - e.vaddr) != value) ++wrong; + }; + auto table = [&](elf::i64 tag, elf::i64 size_tag) { + const auto at = v.offset_of(v.dynamic(tag, ~0ull)); + const auto n = v.dynamic(size_tag); + const auto* r = at == ~0ull ? nullptr : v.at(at, n / sizeof(elf::rela)); + for (size k = 0; r && k < n / sizeof(elf::rela); ++k) + if (elf::r_type(r[k].info) == relative) expect(r[k].offset, p.base + static_cast(r[k].addend)); + }; + table(elf::dt_rela, elf::dt_relasz); + table(elf::dt_jmprel, elf::dt_pltrelsz); + const auto relr = v.offset_of(v.dynamic(elf::dt_relr, ~0ull)); + const auto relr_n = v.dynamic(elf::dt_relrsz) / 8; + const auto* rr = relr == ~0ull ? nullptr : v.at(relr, relr_n); + elf::u64 next = 0; + for (size k = 0; rr && k < relr_n; ++k) { + if ((rr[k] & 1) == 0) { + if (inside(rr[k])) expect(rr[k], word(was, rr[k] - e.vaddr) + p.base); + next = rr[k] + 8; + } else { + elf::u64 bits = rr[k] >> 1; + for (elf::u64 j = 0; bits; ++j, bits >>= 1) + if ((bits & 1) && inside(next + 8 * j)) expect(next + 8 * j, word(was, next + 8 * j - e.vaddr) + p.base); + next += 63 * 8; + } + } + elf::u64 differ = 0; + for (elf::u64 at = 0; at + 8 <= e.size; at += 8) if (word(was, at) != word(now, at)) ++differ; + for (elf::u64 at = e.size & ~elf::u64{7}; at < e.size; ++at) if (was[at] != now[at]) ++differ; + if (wrong != 0 || differ != written) { + c.fail("a placed segment is not its file bytes with the relative relocations applied for the base (SPEC 12.4)"); + return; + } + } +} } // namespace extern "C" int okld_check(const void* bytes, okld_size n, okld_name_test known_kal, @@ -232,6 +286,20 @@ extern "C" int okld_check(const void* bytes, okld_size n, okld_name_test known_k } } + // A PLACEMENT, WHERE ONE IS STATED, DESCRIBES THE IMAGE AND HOLDS ITS + // SEGMENTS AS THE RELATIVE RELOCATIONS COMPUTE THEM FOR ITS BASE (SPEC + // 12.4, type 3): every word that differs from the file's is a relative + // relocation's, with the value it computes, and no other word differs. + elf::u32 psz = 0; + if (const auto* pd = find_note(v, nullptr, placement_note, &psz); pd && psz >= 16) { + elf::u32 count = 0; + copy(&count, pd + 8, 4); + placement p; + if (count != 0 && !read_placement(v, p)) + c.fail("its placement note does not describe its writable segments (SPEC 12.4)"); + else if (count != 0) verify_placement(v, p, c); + } + elf::u32 sz = 0; const auto* req = find_note(v, nullptr, 1, &sz); if (!req || sz < 16) c.fail("has no requirements note (OPENKAL, type 1; SPEC 12.4)"); diff --git a/run/mcpp.toml b/run/mcpp.toml index 1e4c8e0..0742cc8 100644 --- a/run/mcpp.toml +++ b/run/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-run" -version = "0.3.2" +version = "0.4.0" description = "libokrun: the runner's core --- an ok image's names bound to this process's one openkal implementation, in the image's calling convention, and its entry entered." license = "Apache-2.0" authors = ["mcpplibs"] @@ -11,17 +11,17 @@ repo = "https://github.com/mcpplibs/openkal-image" kind = "lib" [dependencies] -openkal = "0.22.0" +openkal = "0.23.0" openkal-ld = { path = "../ld" } [target.linux.dependencies] -openkal-linux = "0.23.0" +openkal-linux = "0.24.0" [target.macos.dependencies] -openkal-macos = "0.20.0" +openkal-macos = "0.21.0" [target.windows.dependencies] -openkal-windows = "0.18.0" +openkal-windows = "0.19.0" # The interfaces each system's implementation provides decide one unit: where # openkal.space is provided its names are bound, and elsewhere an image that diff --git a/run/src/run.cpp b/run/src/run.cpp index cf2c2e5..d59121d 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -51,6 +51,16 @@ bool same_bytes(const void* a, const void* b, unsigned long n) { void* reserve(okld_size n, void*) { return kal_exec_alloc(n); } int publish(void* region, okld_size off, okld_size n, void*) { return kal_exec_publish_part(region, off, n); } void release(void* region, okld_size n, void*) { kal_exec_free(region, n); } +// At the base an image states (openkal 0.23, SPEC 12.3), where the +// implementation places memory where it is asked. OKRUN_PLACE=0 places every +// image anywhere and relocates it, as one that states no base is. +void* reserve_at(void* at, okld_size n, void*) { return kal_exec_alloc_at(at, n); } +bool placing() { + if (!(kal_exec_props() & KAL_EXEC_PROP_PLACE)) return false; + char v[4]; + const kal_intptr n = kal_env_var("OKRUN_PLACE", 11, v, sizeof v); + return !(n == 1 && v[0] == '0'); +} okld_size granularity(void*) { return kal_exec_granularity(); } void* resolve(const char* name, void*) { return okrun_resolve(name); } void* allocate(okld_size n, void*) { return kal_alloc(n, 16); } @@ -616,6 +626,7 @@ void prepare_host(search& s) { host.granularity = &granularity; host.resolve = &resolve; host.alloc = &allocate; host.free = &deallocate; host.find_needed = &find_needed; host.release_bytes = &release_needed; + host.reserve_at = placing() ? &reserve_at : nullptr; host.debugger = s.options ? s.options->debugger : 0; host.ctx = &s; } diff --git a/tools/mcpp.toml b/tools/mcpp.toml index d04a93c..4f25d9b 100644 --- a/tools/mcpp.toml +++ b/tools/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-tools" -version = "0.3.2" +version = "0.4.0" description = "okrun, which runs an ok image on this system, and okld, the format's tool on a build machine (check, note, sign, inspect)." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/tools/src/okld.cpp b/tools/src/okld.cpp index c78147a..b7c1a47 100644 --- a/tools/src/okld.cpp +++ b/tools/src/okld.cpp @@ -4,10 +4,15 @@ // the convention (SPEC 12.2, 12.4); one line per violation // okld note the interfaces the image's imports require, written into // its requirements note +// okld place [--base ] +// each writable segment computed for one base and +// appended, and the placement note filled (SPEC 12.4, +// type 3): a loader that places the image there +// applies no relative relocation // okld sign the signature note filled (Ed25519); prints the key // okld keygen a new seed, written; prints its public key // okld inspect what the image is -// okld note-source the assembly that lays down the two notes, for the link +// okld note-source the assembly that lays down the three notes, for the link // okld bundle [--name ] [--version ] [--entry ] [--sign ] ... // an okb (docs/okb.md): the images, by architecture, and // okb.json; with --sign, each image and okb.json signed @@ -514,19 +519,22 @@ int bundle_check(const std::vector& args) { int main(int argc, char** argv) { const std::vector args(argv + 1, argv + argc); - auto usage = [] { std::println(std::cerr, "usage: okld check|note|inspect | sign | keygen | note-source | bundle ... | bundle check "); return 2; }; + auto usage = [] { std::println(std::cerr, "usage: okld check|note|place|inspect | sign | keygen | note-source | bundle ... | bundle check "); return 2; }; if (args.empty()) return usage(); const auto& verb = args[0]; if (verb == "note-source") { std::print("{}", - "# The two notes of an ok image (SPEC 12.4): requirements, written by `okld note',\n" - "# and a signature, written by `okld sign' and all zero until then.\n" + "# The three notes of an ok image (SPEC 12.4): requirements, written by `okld note';\n" + "# a signature, written by `okld sign'; and a placement, written by `okld place'\n" + "# (format 1.1). The last two are all zero until then.\n" "\t.section .note.openkal,\"a\",@note\n" "\t.p2align 2\n" - "\t.long 8, 16, 1\n\t.asciz \"OPENKAL\"\n\t.long 1, 0\n\t.quad 0\n" + "\t.long 8, 16, 1\n\t.asciz \"OPENKAL\"\n\t.long 1, 1\n\t.quad 0\n" "\t.p2align 2\n" - "\t.long 8, 96, 2\n\t.asciz \"OPENKAL\"\n\t.fill 96, 1, 0\n"); + "\t.long 8, 96, 2\n\t.asciz \"OPENKAL\"\n\t.fill 96, 1, 0\n" + "\t.p2align 2\n" + "\t.long 8, 208, 3\n\t.asciz \"OPENKAL\"\n\t.fill 208, 1, 0\n"); return 0; } if (verb == "keygen" && args.size() == 2) { @@ -564,6 +572,23 @@ int main(int argc, char** argv) { std::println("{}: requires {}", args[1], interfaces(bits)); return 0; } + if (verb == "place" && (args.size() == 2 || (args.size() == 4 && args[2] == "--base"))) { + okld_u64 base = 0; + if (args.size() == 4) base = std::stoull(args[3], nullptr, 16); + okld_why why{}; + const okld_u64 need = okld_place(image->data(), image->size(), base, nullptr, 0, &why); + if (need == 0) { std::println(std::cerr, "okld: {}: {}", args[1], why.text); return 1; } + std::vector placed(need); + if (okld_place(image->data(), image->size(), base, placed.data(), need, &why) != need) { + std::println(std::cerr, "okld: {}: {}", args[1], why.text); + return 1; + } + if (!write_file(args[1], placed)) return 1; + okld_u64 at = 0; okld_u32 count = 0; + okld_placement(placed.data(), placed.size(), &at, &count); + std::println("{}: placed at {:#x}, {} writable segment(s) stored", args[1], at, count); + return 0; + } if (verb == "sign" && args.size() == 3) { auto seed = read_file(args[2]); if (!seed || seed->size() != 32) { std::println(std::cerr, "okld: {} is not a 32-byte seed", args[2]); return 1; } @@ -586,6 +611,11 @@ int main(int argc, char** argv) { const int v = okld_verify(image->data(), image->size(), key); std::println("signature {}", v == 1 ? "valid, by " + hex(key, 32) : v == 0 ? std::string("none") : std::string("PRESENT AND WRONG")); std::println("span {:#x}", info.span); + okld_u64 base = 0; okld_u32 count = 0; + if (okld_placement(image->data(), image->size(), &base, &count) == 1) + std::println("placement {:#x}, {} writable segment(s) stored", base, count); + else + std::println("placement none"); std::println("entry {:#x}", info.entry); std::println("needs {} image(s)", info.needed_count); std::vector imports; From 4ed649cb094b6de432aae1c2d8329d442be23bdd Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 06:56:56 +0900 Subject: [PATCH 2/9] wip: per-boot placement, prepared copies, touches, profile reports, debugger scripts Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- ld/include/okld.h | 116 ++++- ld/src/image.h | 3 + ld/src/load.cpp | 337 ++++++++++++--- ld/src/okld.cppm | 14 + ld/src/place.cpp | 180 ++++++++ ld/src/read.cpp | 31 +- run/build.mcpp | 39 ++ run/src/bindings.cpp | 38 +- run/src/prof.cpp | 256 ++++++++++- run/src/run.cpp | 399 ++++++++++++++++-- run/src/runner.h | 9 + .../__pycache__/okgdb_common.cpython-312.pyc | Bin 0 -> 5465 bytes tools/scripts/okgdb.py | 62 +++ tools/scripts/okgdb_common.py | 88 ++++ tools/scripts/oklldb.py | 50 +++ tools/src/okld.cpp | 186 +++++++- tools/src/okprof.cpp | 351 ++++++++++++++- 17 files changed, 2026 insertions(+), 133 deletions(-) create mode 100644 tools/scripts/__pycache__/okgdb_common.cpython-312.pyc create mode 100644 tools/scripts/okgdb.py create mode 100644 tools/scripts/okgdb_common.py create mode 100644 tools/scripts/oklldb.py diff --git a/ld/include/okld.h b/ld/include/okld.h index f4e7944..e7156ed 100644 --- a/ld/include/okld.h +++ b/ld/include/okld.h @@ -54,6 +54,15 @@ typedef void (*okld_report)(const char* sentence, void* ctx); int okld_check(const void* bytes, okld_size size, okld_name_test known_kal, okld_name_test exported_elsewhere, okld_report report, void* ctx); +/* What an image's placement holds (SPEC 12.4, type 3): each stored segment is + * its file bytes with every relative relocation applied for the base, and no + * other word differs. A pass over every relative relocation and every stored + * byte, asked when an image is sealed (`okld check'); okld_check, which the + * loader asks on every load, holds only that the placement describes the image. + * Returns how many did not hold, calling `report' with each; 0 for an image + * that states no placement. Version 0.4. */ +int okld_check_placement(const void* bytes, okld_size size, okld_report report, void* ctx); + /* The names an image imports, and the names it exports, one call each. */ void okld_imports(const void* bytes, okld_size size, void (*each)(const char* name, void* ctx), void* ctx); /* The names an image requires: its imports less its weak ones. A weak import @@ -246,14 +255,15 @@ struct okld_host { void (*free)(void* p, okld_size bytes, void* ctx); /* Makes part of the region the bytes of the file the image was read from, * rather than a copy of them (kal_exec_map_part, openkal 0.21): `size' - * bytes from `bytes' + `from', at `offset' in the region, executed where - * `execute' is non-zero. `from' is a multiple of 65536 (KAL_MAP_ALIGNMENT) - * and `offset' and `size' of the granularity. Zero when it did; anything - * else, and the loader copies the bytes, as it does where this is null --- - * for bytes that are not a file of their own, or an implementation that - * does not report KAL_EXEC_PROP_FILE. Version 0.3. */ + * bytes from `bytes' + `from', at `offset' in the region, as `how' says + * (OKLD_MAP_*; 0 read, as it was 0 in version 0.3, and 1 executed). `from' + * is a multiple of 65536 (KAL_MAP_ALIGNMENT) and `offset' and `size' of + * the granularity. Zero when it did; anything else, and the loader copies + * the bytes, as it does where this is null --- for bytes that are not a file + * of their own, or an implementation that does not report + * KAL_EXEC_PROP_FILE (or, for OKLD_MAP_WRITE, openkal 0.23). Version 0.3. */ int (*map)(void* region, okld_size offset, okld_size size, const void* bytes, okld_size from, - int execute, void* ctx); + int how, void* ctx); /* Non-zero: each image is announced to a debugger through the GDB JIT * interface (`__jit_debug_register_code'), with its addresses where it was * placed, so that gdb and lldb show its symbols and its source. */ @@ -266,8 +276,27 @@ struct okld_host { * relocations are not applied; null, or refused, and the image is reserved * anywhere and relocated. Version 0.4. */ void* (*reserve_at)(void* at, okld_size bytes, void* ctx); + /* Makes a part read-only once its relocations are applied + * (kal_exec_seal_part, openkal 0.23): the image's read-only-after- + * relocation range, for good where `forever' is non-zero. Null, and the + * range stays writable, as it was before version 0.4. */ + int (*seal)(void* region, okld_size offset, okld_size size, int forever, void* ctx); + /* Non-zero: the modules loaded with this host are never unloaded --- a + * runner's entry image and those it needs --- so their read-only-after- + * relocation range is sealed for good. Version 0.4. */ + int permanent; + /* Non-zero: the pages an image states it touches when it starts (SPEC + * 12.4, type 4) are read in as their part is mapped (OKLD_MAP_PREFETCH); + * zero spends no memory ahead of the program. A runner sets it by the + * program's resource preference. Version 0.4. */ + int prefetch; }; +/* What `okld_host::map' is asked. Version 0.4. */ +#define OKLD_MAP_EXECUTE 1 /* executed (KAL_EXEC_MAP_EXECUTE) */ +#define OKLD_MAP_PREFETCH 2 /* read in now (KAL_EXEC_MAP_PREFETCH) */ +#define OKLD_MAP_WRITE 4 /* written, privately (KAL_EXEC_MAP_WRITE) */ + struct okld_module; /* --- placement (SPEC 12.4, type 3; openkal 0.23) ---------------------------- */ @@ -296,6 +325,70 @@ okld_u64 okld_place(const void* image, okld_size size, okld_u64 base, void* out, * entries (0 when it states none). */ int okld_placement(const void* image, okld_size size, okld_u64* base, okld_u32* count); +/* --- the touch note (SPEC 12.4, type 4) ------------------------------------- */ + +/* The ranges a touch note has room for: `okld note-source' lays down this many. */ +#define OKLD_TOUCH_MAX 512u + +/* Writes the touch note in place: `count' ranges, each two okld_u32 --- the + * first 65536-byte block of the range in the file and the number of blocks --- + * in the order the image first touches them when it starts. 0, or negative + * with `why' filled: no note to write into, more ranges than it holds, or a + * signed image (the signature is over these bytes: note, then sign). */ +int okld_touch(void* image, okld_size size, const okld_u32* ranges, okld_u32 count, struct okld_why* why); +/* The ranges the touch note states, up to `cap' (pairs into `out'); returns + * how many it states. */ +okld_u32 okld_touches(const void* image, okld_size size, okld_u32* out, okld_u32 cap); + +/* --- a loader's prepared copy (SPEC 12.4, type 5) ---------------------------- */ + +/* A FILE A LOADER WRITES FOR ITSELF AND DOES NOT DISTRIBUTE: a note of type 5 + * at its start, then blocks of bytes at multiples of 65536 that are, at their + * addresses, what the image's writable segments hold --- computed for another + * base than the one the image was sealed with (a runner's base of this boot), + * or, with OKLD_PREPARED_SNAPSHOT, the image's state after its initialisers + * ran, with the memory they obtained (the arena, after the image's span). The + * loader takes the image's code from the image and these blocks for its data, + * mapping each block it can and copying the rest. */ +#define OKLD_PREPARED_SNAPSHOT 1u + +struct okld_prepared_block { + okld_u64 address; /* where the bytes go */ + okld_u64 offset; /* where they are in the prepared file */ + okld_u64 size; +}; + +struct okld_prepared_info { + okld_u32 version; /* 1 */ + okld_u32 flags; /* OKLD_PREPARED_* */ + okld_u64 base; /* the image's base */ + unsigned char digest[32]; /* the image, by the runner's name for it (SHA-256) */ + okld_u32 count; /* blocks */ + okld_u32 reserved; + okld_u64 arena; /* snapshot: the arena's address, after the image's span */ + okld_u64 arena_size; /* snapshot: the bytes of it the state reaches */ + okld_u64 context; /* snapshot: the word of the context the initialisers ran on */ + okld_u64 start; /* snapshot: the second phase's entry, relative to the base */ + okld_u64 state; /* snapshot: `__okm_snapshot_arena', relative to the base */ +}; + +/* The image's writable segments computed for `base': their file bytes with + * every relative relocation applied. `digest' is the name the caller gives + * the image. Writes into `out' when `cap' holds it and returns the size, as + * okld_place does; 0 with `why' filled when the image has no writable segment + * or the base is not a multiple of 65536. */ +okld_u64 okld_prepare(const void* image, okld_size size, okld_u64 base, const unsigned char digest[32], + void* out, okld_u64 cap, struct okld_why* why); +/* Writes a prepared file of `info' and `blocks', each block's bytes from + * `source(address)' --- the whole of a snapshot's --- as okld_prepare does. */ +okld_u64 okld_prepare_blocks(const struct okld_prepared_info* info, const struct okld_prepared_block* blocks, + const void* (*source)(okld_u64 address, okld_u64 size, void* ctx), void* ctx, + void* out, okld_u64 cap); +/* Reads a prepared file's note: 0, or negative when it is not one. Its blocks, + * up to `cap', into `blocks'. */ +int okld_prepared(const void* prepared, okld_size size, struct okld_prepared_info* info, + struct okld_prepared_block* blocks, okld_u32 cap); + /* Maps, relocates and publishes an image and, first, every image it needs. * Initialisers are not run: `okld_initialise' runs them, so that a caller can * do what it must between. The loader keeps no pointer into `bytes', nor into @@ -303,6 +396,15 @@ int okld_placement(const void* image, okld_size size, okld_u64* base, okld_u32* * (version 0.1.6; a runner releases the file it ran). */ int okld_load(const void* bytes, okld_size size, const char* name, const struct okld_host* host, struct okld_module** out, struct okld_why* why); +/* okld_load, the image's data taken from a prepared copy (type 5) of it: at the + * copy's base, which the host reserves (reserve_at) or the load is refused; + * with a snapshot, its arena reserved after the span and its initialisers + * counted as run. The copy is a file of its own: `okld_host::map' is given its + * bytes. Its digest is not compared with the image's: the caller named both. + * Version 0.4. */ +int okld_load_prepared(const void* bytes, okld_size size, const char* name, const struct okld_host* host, + const void* prepared, okld_size prepared_size, struct okld_module** out, + struct okld_why* why); /* Runs the initialisers of the module and of the modules it needs, needed ones * first, each once. `call' enters a function of the image in the image's * convention (the runner supplies it; see okrun). */ diff --git a/ld/src/image.h b/ld/src/image.h index d8cdcda..547b049 100644 --- a/ld/src/image.h +++ b/ld/src/image.h @@ -78,6 +78,9 @@ struct placement { entry e[8]; }; constexpr elf::u32 placement_note = 3; +// THE TOUCH NOTE (type 4) and A LOADER'S PREPARED COPY (type 5), version 0.4. +constexpr elf::u32 touch_note = 4; +constexpr elf::u32 prepared_note = 5; constexpr size placement_descriptor = 16 + 8 * sizeof(placement::entry); // The placement an image states, read and held to the image: one entry for diff --git a/ld/src/load.cpp b/ld/src/load.cpp index 1c997aa..f02bcd7 100644 --- a/ld/src/load.cpp +++ b/ld/src/load.cpp @@ -303,65 +303,169 @@ void discard(okld_module* m) { } // namespace namespace { -// A WRITABLE SEGMENT OF AN IMAGE AT ITS BASE: the bytes the image stores for -// that base (SPEC 12.4, type 3). Each 64 KiB of it that lies wholly within the -// read-only-after-relocation range, and that no relocation naming a symbol -// writes, is made the file's pages where the host can (kal_exec_map_part): it -// is never written again, as the same data of a program linked for one address -// is not. The rest is copied, and the size in memory beyond the file's is zero. -void place_segment(okld_module* m, const view& v, const okld_host* host, const elf::phdr& p, - const placement& at, elf::u64 relro_from, elf::u64 relro_to, const void* bytes) { - const placement::entry* e = nullptr; - for (elf::u32 i = 0; i < at.count; ++i) if (at.e[i].vaddr == p.vaddr) e = &at.e[i]; - const auto* stored = static_cast(bytes) + e->offset; - // The 64 KiB blocks a relocation naming a symbol writes, which are copied. - constexpr elf::u64 block = file_alignment; - const elf::u64 first = round_down(p.vaddr, block), blocks = (round_up(p.vaddr + p.filesz, block) - first) / block; - bool named[512] = {}; - const bool mappable = host->map && blocks <= 512 && relro_to > relro_from; - if (mappable) { - auto mark = [&](elf::i64 tag, elf::i64 size_tag) { - const auto at_ = v.offset_of(v.dynamic(tag, ~0ull)); - const auto n = v.dynamic(size_tag); - const auto* r = at_ == ~0ull ? nullptr : v.at(at_, n / sizeof(elf::rela)); - for (size k = 0; r && k < n / sizeof(elf::rela); ++k) { - if (!names_a_symbol(v, elf::r_type(r[k].info))) continue; - const elf::u64 o = r[k].offset; - if (o + 8 <= p.vaddr || o >= p.vaddr + p.filesz) continue; - named[(o > first ? o - first : 0) / block] = true; - const elf::u64 last = (o + 7 - first) / block; - if (last < blocks) named[last] = true; +// WHERE AN IMAGE'S DATA COMES FROM WHEN IT IS AT A BASE: blocks of bytes that +// are, at their addresses, what its writable segments hold --- the stored +// segments of its placement (SPEC 12.4, type 3), in its own file, or a +// loader's prepared copy (type 5), a file of its own --- and, for a snapshot, +// the memory its initialisers obtained. +struct source { + const unsigned char* bytes = nullptr; // the file the blocks are in + size n = 0; + elf::u64 base = 0; + okld_prepared_block block[64]; + elf::u32 count = 0; + bool snapshot = false; + elf::u64 arena = 0, arena_size = 0; +}; + +// The 64 KiB blocks of a writable segment a relocation naming a symbol writes: +// they are written on every start, so they are not a file's read-only pages. +struct named_blocks { + elf::u64 first = 0; + bool named[1024] = {}; + elf::u64 count = 0; +}; + +constexpr elf::u64 block_size = 65536; + +void mark_named(const view& v, elf::u64 from, elf::u64 to, named_blocks& out) { + out.first = round_down(from, block_size); + out.count = (round_up(to, block_size) - out.first) / block_size; + if (out.count > 1024) out.count = 1024; + auto mark = [&](elf::i64 tag, elf::i64 size_tag) { + const auto at = v.offset_of(v.dynamic(tag, ~0ull)); + const auto n = v.dynamic(size_tag); + const auto* r = at == ~0ull ? nullptr : v.at(at, n / sizeof(elf::rela)); + for (size k = 0; r && k < n / sizeof(elf::rela); ++k) { + if (!names_a_symbol(v, elf::r_type(r[k].info))) continue; + const elf::u64 o = r[k].offset; + if (o + 8 <= from || o >= to) continue; + const elf::u64 a = (o > out.first ? o - out.first : 0) / block_size; + const elf::u64 b = (o + 7 - out.first) / block_size; + if (a < out.count) out.named[a] = true; + if (b < out.count) out.named[b] = true; + } + }; + mark(elf::dt_rela, elf::dt_relasz); + mark(elf::dt_jmprel, elf::dt_pltrelsz); +} + +// A block's bytes into the region at `region', which begins at `at' (the +// image's base, or a snapshot's arena) and holds `span' bytes: each 64 KiB of +// it the block covers whole is the file's pages where the host can make it so +// --- written privately, or read only where it lies in the read-only-after- +// relocation range and no relocation naming a symbol writes it --- and the rest +// is copied. A run of blocks alike is one request. +void place_block(const okld_host* host, unsigned char* region, elf::u64 at, elf::u64 span, + const source& src, const okld_prepared_block& b, elf::u64 relro_from, elf::u64 relro_to, + const named_blocks& named) { + const elf::u64 lo = b.address - at, hi = lo + b.size; // in the region + const elf::u64 g = host->granularity(host->ctx); + auto how_of = [&](elf::u64 chunk) -> int { + const elf::u64 image_lo = chunk + at - src.base; // as an address of the image + const bool relro = image_lo >= relro_from && image_lo + block_size <= relro_to; + const elf::u64 k = (image_lo - named.first) / block_size; + const bool written = image_lo < named.first || k >= named.count || named.named[k]; + return relro && !written ? 0 : OKLD_MAP_WRITE; + }; + elf::u64 c = round_up(lo, block_size); + // The head before the first whole block, copied. + const elf::u64 head_end = c < hi ? c : hi; + if (head_end > lo) copy(region + lo, src.bytes + b.offset, head_end - lo); + while (c < hi) { + const elf::u64 room = hi - c; + // The tail of the block: whole to the granularity where it ends the + // span's writable part, copied otherwise. + elf::u64 len = room >= block_size ? block_size : 0; + if (len == 0) { + const elf::u64 rounded = round_up(room, g); + len = c + rounded <= span ? rounded : 0; + } + bool mapped = false; + if (len != 0 && host->map) { + const int how = how_of(c); + elf::u64 run = len; + while (run % block_size == 0 && c + run + block_size <= hi && how_of(c + run) == how) run += block_size; + if (c + run < hi && hi - (c + run) < block_size) { + const elf::u64 tail = round_up(hi - (c + run), g); + if (c + run + tail <= span && how_of(c + run) == how) run += tail; } - }; - mark(elf::dt_rela, elf::dt_relasz); - mark(elf::dt_jmprel, elf::dt_pltrelsz); - } - for (elf::u64 b = 0; b < blocks; ++b) { - const elf::u64 lo = first + b * block, hi = lo + block; - const elf::u64 from = lo > p.vaddr ? lo : p.vaddr; - const elf::u64 to = hi < p.vaddr + p.filesz ? hi : p.vaddr + p.filesz; - // A run of blocks the file can hold, mapped together. - if (mappable && lo >= p.vaddr && hi <= p.vaddr + p.filesz && lo >= relro_from && hi <= relro_to && !named[b]) { - elf::u64 end = b + 1; - while (end < blocks) { - const elf::u64 elo = first + end * block, ehi = elo + block; - if (ehi > p.vaddr + p.filesz || ehi > relro_to || named[end]) break; - ++end; + if (host->map(region, c, run, src.bytes, b.offset + (c - lo), how, host->ctx) == 0) { + mapped = true; + c += run; } - const elf::u64 bytes_ = (end - b) * block; - if (host->map(m->base, lo, bytes_, bytes, e->offset + (lo - p.vaddr), 0, host->ctx) == 0) { - b = end - 1; - continue; + } + if (!mapped) { + const elf::u64 n = room < block_size ? room : block_size; + copy(region + c, src.bytes + b.offset + (c - lo), n); + c += n; + } + } +} + +// The pages an image states it touches when it starts (SPEC 12.4, type 4), as +// a set of the 64 KiB blocks of its file. +struct touched { + okld_u32 ranges[2 * OKLD_TOUCH_MAX]; + okld_u32 count = 0; + bool holds(elf::u64 file_block) const { + for (okld_u32 i = 0; i < count; ++i) + if (file_block >= ranges[2 * i] && file_block < static_cast(ranges[2 * i]) + ranges[2 * i + 1]) + return true; + return false; + } +}; + +// A segment that is the file's pages: in runs that are read in now and runs +// that are not, where the image states its touches and the host reads ahead. +// Runs a few blocks apart are joined: a request costs more than the pages. +int map_segment(const okld_host* host, unsigned char* region, elf::u64 lo, elf::u64 hi, const void* bytes, + elf::u64 from, int how, const touched* t) { + if (!t || t->count == 0) return host->map(region, lo, hi - lo, bytes, from, how, host->ctx); + const elf::u64 blocks = (hi - lo + block_size - 1) / block_size; + auto is_touched = [&](elf::u64 k) { return t->holds((from + k * block_size) / block_size); }; + elf::u64 k = 0; + while (k < blocks) { + const bool on = is_touched(k); + elf::u64 e = k + 1; + for (;;) { + while (e < blocks && is_touched(e) == on) ++e; + // A gap of at most two blocks inside a run read in is read in too. + if (on && e < blocks) { + elf::u64 g2 = e; + while (g2 < blocks && !is_touched(g2) && g2 - e < 3) ++g2; + if (g2 < blocks && g2 - e < 3 && is_touched(g2)) { e = g2; continue; } } + break; } - copy(m->base + from, stored + (from - p.vaddr), to - from); + const elf::u64 a = lo + k * block_size, z = e == blocks ? hi : lo + e * block_size; + if (host->map(region, a, z - a, bytes, from + k * block_size, how | (on ? OKLD_MAP_PREFETCH : 0), host->ctx) != 0) + return -1; + k = e; } - fill(m->base + p.vaddr + p.filesz, 0, p.memsz - p.filesz); + return 0; +} + +// The placement an image states, as a source in its own file. +bool placement_source(const view& v, const placement& p, const void* bytes, source& out) { + out.bytes = static_cast(bytes); + out.n = v.len; + out.base = p.base; + out.count = 0; + for (elf::u32 i = 0; i < p.count && out.count < 64; ++i) + out.block[out.count++] = okld_prepared_block{ p.base + p.e[i].vaddr, p.e[i].offset, p.e[i].size }; + return true; } } // namespace -extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const okld_host* host, - okld_module** out, okld_why* why) { +namespace { +int load_image(const void* bytes, okld_size n, const char* name, const okld_host* host, const source* prepared, + okld_module** out, okld_why* why); +} // namespace + +namespace { +int load_image(const void* bytes, okld_size n, const char* name, const okld_host* host, const source* prepared, + okld_module** out, okld_why* why) { view v; if (!open(bytes, n, v, why ? why->text : nullptr, why ? sizeof why->text : 0)) return -1; if (char reason[256]; okld_check(bytes, n, nullptr, [](const char*, void*) { return 1; }, @@ -393,7 +497,7 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const const void* nbytes = host->find_needed ? host->find_needed(need, &nsize, host->ctx) : nullptr; if (!nbytes) { say(why, "the image needs '", need, "', which is not found"); host->free(m, sizeof *m, host->ctx); return -1; } okld_module* nm = nullptr; - const int rc = okld_load(nbytes, nsize, need, host, &nm, why); + const int rc = load_image(nbytes, nsize, need, host, nullptr, &nm, why); if (host->release_bytes) host->release_bytes(nbytes, nsize, host->ctx); if (rc != 0) { host->free(m, sizeof *m, host->ctx); return rc; } m->needed[m->needed_count++] = nm; @@ -406,19 +510,47 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const m->span = round_up(m->span, g); // AT ITS BASE WHERE THE IMAGE STATES ONE AND THE HOST CAN PLACE IT THERE // (SPEC 12.3, openkal 0.23): its writable segments are then the bytes it - // stores for that base, and no relative relocation is applied. Otherwise - // anywhere, and every relocation is. + // stores for that base, and no relative relocation is applied. A prepared + // copy (type 5, 0.4) brings a base of its own and the data for it, and is + // refused rather than placed elsewhere: the bytes are for that base alone. + // Otherwise anywhere, and every relocation is. placement placed_at; - bool placed = false; - if (host->reserve_at && read_placement(v, placed_at) && placed_at.base % g == 0) { + source own; + const source* src = nullptr; + if (prepared) { + if (!host->reserve_at || prepared->base % g != 0) { + say(why, "the prepared copy's base cannot be reserved here"); + host->free(m, sizeof *m, host->ctx); + return -1; + } + m->base = static_cast(host->reserve_at(reinterpret_cast(prepared->base), m->span, host->ctx)); + if (!m->base) { say(why, "the prepared copy's base is not free"); host->free(m, sizeof *m, host->ctx); return -1; } + src = prepared; + } else if (host->reserve_at && read_placement(v, placed_at) && placed_at.base % g == 0) { m->base = static_cast(host->reserve_at(reinterpret_cast(placed_at.base), m->span, host->ctx)); - placed = m->base != nullptr; + if (m->base) { placement_source(v, placed_at, bytes, own); src = &own; } } + const bool placed = src != nullptr; if (!placed) m->base = static_cast(host->reserve(m->span, host->ctx)); if (!m->base) { say(why, "the address space for the image could not be reserved"); host->free(m, sizeof *m, host->ctx); return -1; } - // Each segment is its file bytes and then zeros to its size in memory. What - // lies between segments is no segment's and is not written: a page the image - // never touches is not made resident. + // A SNAPSHOT'S ARENA, after the span: the memory its initialisers obtained, + // at the addresses they were given. + unsigned char* arena = nullptr; + if (placed && src->snapshot && src->arena_size != 0) { + arena = static_cast(host->reserve_at(reinterpret_cast(src->arena), src->arena_size, host->ctx)); + if (!arena) { + say(why, "the snapshot's arena is not free"); + host->release(m->base, m->span, host->ctx); + host->free(m, sizeof *m, host->ctx); + return -1; + } + } + touched t; + if (host->prefetch) t.count = okld_touches(bytes, n, t.ranges, OKLD_TOUCH_MAX); + // Each segment is its file bytes and then zeros to its size in memory: the + // region is zero when it is returned (openkal 0.23), so the zeros are not + // written and a page of them is resident only once the program touches it. + // What lies between segments is no segment's and is not written either. // // A SEGMENT NOTHING WRITES IS THE FILE'S OWN PAGES where the host can make // it so (0.3, openkal 0.21): instructions and read-only data are the file's @@ -443,10 +575,8 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const if (p.flags & elf::pf_w) { if (w.count == 8) { say(why, "the image has more than eight writable segments"); host->release(m->base, m->span, host->ctx); host->free(m, sizeof *m, host->ctx); return -1; } w.from[w.count] = p.vaddr; w.to[w.count] = p.vaddr + p.memsz; ++w.count; - if (placed && p.filesz != 0) { - place_segment(m, v, host, p, placed_at, relro_from, relro_to, bytes); - continue; - } + // AT A BASE THE DATA IS THE SOURCE'S (below), not the file's bytes. + if (placed) continue; } else if (host->map && k < 32 && p.filesz == p.memsz && p.filesz != 0 && p.vaddr % file_alignment == p.offset % file_alignment) { const elf::u64 lo = round_down(p.vaddr, file_alignment), hi = round_up(p.vaddr + p.filesz, g); @@ -458,13 +588,47 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const const elf::u64 qlo = round_down(q.vaddr, g), qhi = round_up(q.vaddr + q.memsz, g); if (qlo < hi && lo < qhi) alone = false; } - if (alone && host->map(m->base, lo, hi - lo, bytes, from, (p.flags & elf::pf_x) != 0, host->ctx) == 0) { + if (alone && map_segment(host, m->base, lo, hi, bytes, from, (p.flags & elf::pf_x) ? OKLD_MAP_EXECUTE : 0, + host->prefetch ? &t : nullptr) == 0) { mapped[k] = true; continue; } } copy(m->base + p.vaddr, static_cast(bytes) + p.offset, p.filesz); - fill(m->base + p.vaddr + p.filesz, 0, p.memsz - p.filesz); + } + // THE DATA OF AN IMAGE AT A BASE (0.4): each block of the source at its + // address, the file's pages written privately where the host can make them + // so (openkal 0.23, KAL_EXEC_MAP_WRITE) --- shared until the program writes + // a page, as a program of the environment's data is --- and copied where it + // cannot. A snapshot's blocks are the whole of the data and of the arena. + if (placed) { + // The blocks a relocation naming a symbol writes, over the writable + // segments together: those are not a file's read-only pages. + named_blocks named; + elf::u64 wlo = ~0ull, whi = 0; + for (elf::u32 k = 0; k < w.count; ++k) { + if (w.from[k] < wlo) wlo = w.from[k]; + if (w.to[k] > whi) whi = w.to[k]; + } + if (w.count) mark_named(v, wlo, whi, named); + for (elf::u32 i = 0; i < src->count; ++i) { + const okld_prepared_block& b = src->block[i]; + const bool in_arena = arena && b.address >= src->arena && b.address - src->arena < src->arena_size; + if (in_arena) { + if (b.size > src->arena_size - (b.address - src->arena)) continue; + place_block(host, arena, src->arena, src->arena_size, *src, b, 0, 0, named); + } else { + const elf::u64 at = b.address - src->base; + if (b.address < src->base || !w.holds(at, b.size)) { + say(why, "a block of the prepared data lies outside the image's writable segments"); + if (arena) host->release(arena, src->arena_size, host->ctx); + host->release(m->base, m->span, host->ctx); + host->free(m, sizeof *m, host->ctx); + return -1; + } + place_block(host, m->base, src->base, m->span, *src, b, relro_from, relro_to, named); + } + } } m->entry = v.eh->entry; m->phnum = v.eh->phnum; @@ -492,6 +656,18 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const if (relocate(m, v, host, w, placed, why) != 0) { discard(m); return -1; } + // READ-ONLY AFTER RELOCATION (openkal 0.23): the addresses of functions and + // data relocation wrote, made read-only now, as a program of the + // environment's are by its loader --- for good where the image is never + // unloaded. The whole pages of the range: its first and last may share a + // page with data that is written. + if (host->seal && relro_to > relro_from) { + const elf::u64 lo = round_up(relro_from, g), hi = round_down(relro_to, g); + if (hi > lo) host->seal(m->base, lo, hi - lo, host->permanent, host->ctx); + } + // A SNAPSHOT'S INITIALISERS HAVE RUN: what they made is its data. + if (placed && src->snapshot) m->initialised = true; + // Publish what executes and was copied; the rest stays writable. A segment // that is the file's pages is executable already, and its bytes reached // memory through the environment rather than through this loader's data @@ -516,6 +692,35 @@ extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const *out = m; return 0; } +} // namespace + +extern "C" int okld_load(const void* bytes, okld_size n, const char* name, const okld_host* host, + okld_module** out, okld_why* why) { + return load_image(bytes, n, name, host, nullptr, out, why); +} + +extern "C" int okld_load_prepared(const void* bytes, okld_size n, const char* name, const okld_host* host, + const void* prepared, okld_size prepared_size, okld_module** out, okld_why* why) { + okld_prepared_info info{}; + source src; + if (okld_prepared(prepared, prepared_size, &info, src.block, 64) != 0 || info.count > 64) { + say(why, "not a prepared copy of an image (SPEC 12.4, type 5)"); + return -1; + } + src.bytes = static_cast(prepared); + src.n = prepared_size; + src.base = info.base; + src.count = info.count; + src.snapshot = (info.flags & OKLD_PREPARED_SNAPSHOT) != 0; + src.arena = info.arena; + src.arena_size = info.arena_size; + for (elf::u32 i = 0; i < src.count; ++i) + if (src.block[i].offset > prepared_size || src.block[i].size > prepared_size - src.block[i].offset) { + say(why, "a block of the prepared copy lies beyond its file"); + return -1; + } + return load_image(bytes, n, name, host, &src, out, why); +} namespace { void run_init(okld_module* m, void (*call)(void*, void*), void* ctx) { diff --git a/ld/src/okld.cppm b/ld/src/okld.cppm index 304b3a7..74e92f9 100644 --- a/ld/src/okld.cppm +++ b/ld/src/okld.cppm @@ -16,6 +16,7 @@ export using ::okld_name_test; export using ::okld_report; export using ::okld_read; export using ::okld_check; +export using ::okld_check_placement; export using ::okld_imports; export using ::okld_required_imports; export using ::okld_exports; @@ -37,6 +38,14 @@ export using ::okld_containing; export using ::okld_place_base; export using ::okld_place; export using ::okld_placement; +export using ::okld_touch; +export using ::okld_touches; +export using ::okld_prepared_block; +export using ::okld_prepared_info; +export using ::okld_prepare; +export using ::okld_prepare_blocks; +export using ::okld_prepared; +export using ::okld_load_prepared; export using ::okld_sign_message; export using ::okld_verify_message; export using ::okld_sha256; @@ -65,4 +74,9 @@ inline constexpr okld_u32 format_minor = OKLD_FORMAT_MINOR; inline constexpr okld_u32 okb_version = OKLD_OKB_VERSION; inline constexpr okld_u32 okb_alignment = OKLD_OKB_ALIGNMENT; inline constexpr okld_u32 okb_chunk = OKLD_OKB_CHUNK; +inline constexpr okld_u32 touch_max = OKLD_TOUCH_MAX; +inline constexpr int map_execute = OKLD_MAP_EXECUTE; +inline constexpr int map_prefetch = OKLD_MAP_PREFETCH; +inline constexpr int map_write = OKLD_MAP_WRITE; +inline constexpr okld_u32 prepared_snapshot = OKLD_PREPARED_SNAPSHOT; } diff --git a/ld/src/place.cpp b/ld/src/place.cpp index f9c97ff..e83d4e8 100644 --- a/ld/src/place.cpp +++ b/ld/src/place.cpp @@ -231,3 +231,183 @@ extern "C" int okld_placement(const void* image, okld_size size, okld_u64* base, if (count) *count = p.count; return 1; } + +// --- the touch note (SPEC 12.4, type 4) ------------------------------------------- + +extern "C" int okld_touch(void* image, okld_size n, const okld_u32* ranges, okld_u32 count, okld_why* why) { + view v; + if (!open(image, n, v, why->text, sizeof why->text)) return -1; + elf::u32 sz = 0; + unsigned char* d = find_note(v, static_cast(image), touch_note, &sz); + if (!d || sz < 8) { + sentence(why->text, sizeof why->text) + << "no touch note to write into (SPEC 12.4, type 4): link the note object `okld note-source' writes"; + return -1; + } + if (8 + static_cast(count) * 8 > sz) { + sentence(why->text, sizeof why->text) << "more ranges than the touch note holds"; + return -1; + } + elf::u32 ssz = 0; + if (const unsigned char* sig = find_note(v, nullptr, 2, &ssz)) { + for (elf::u32 i = 0; i < ssz; ++i) + if (sig[i]) { + sentence(why->text, sizeof why->text) << "the image is signed, and its signature is over the touch note: note, then sign"; + return -1; + } + } + fill(d, 0, sz); + const elf::u32 zero = 0; + copy(d, &count, 4); + copy(d + 4, &zero, 4); + copy(d + 8, ranges, static_cast(count) * 8); + return 0; +} + +extern "C" okld_u32 okld_touches(const void* image, okld_size n, okld_u32* out, okld_u32 cap) { + view v; + char why[8]; + if (!open(image, n, v, why, sizeof why)) return 0; + elf::u32 sz = 0; + const unsigned char* d = find_note(v, nullptr, touch_note, &sz); + if (!d || sz < 8) return 0; + elf::u32 count = 0; + copy(&count, d, 4); + if (8 + static_cast(count) * 8 > sz) return 0; + const elf::u32 given = count < cap ? count : cap; + if (out) copy(out, d + 8, static_cast(given) * 8); + return count; +} + +// --- a loader's prepared copy (SPEC 12.4, type 5) ------------------------------------- +// +// THE FILE: a note of type 5 at its start --- its descriptor the fields of +// okld_prepared_info, then a block entry of three words for each block --- and +// the blocks' bytes, each at an offset congruent to its address modulo 65536, +// so that a loader maps it as it maps an image's own file. + +namespace { +constexpr size prepared_fixed = 4 + 4 + 8 + 32 + 4 + 4 + 8 * 5; // the descriptor before its blocks + +void put_info(unsigned char* d, const okld_prepared_info& i) { + size at = 0; + auto put = [&](const void* p, size k) { copy(d + at, p, k); at += k; }; + put(&i.version, 4); put(&i.flags, 4); put(&i.base, 8); put(i.digest, 32); put(&i.count, 4); put(&i.reserved, 4); + put(&i.arena, 8); put(&i.arena_size, 8); put(&i.context, 8); put(&i.start, 8); put(&i.state, 8); +} + +void get_info(const unsigned char* d, okld_prepared_info& i) { + size at = 0; + auto get = [&](void* p, size k) { copy(p, d + at, k); at += k; }; + get(&i.version, 4); get(&i.flags, 4); get(&i.base, 8); get(i.digest, 32); get(&i.count, 4); get(&i.reserved, 4); + get(&i.arena, 8); get(&i.arena_size, 8); get(&i.context, 8); get(&i.start, 8); get(&i.state, 8); +} +} // namespace + +extern "C" okld_u64 okld_prepare_blocks(const okld_prepared_info* info, const okld_prepared_block* blocks, + const void* (*source)(okld_u64, okld_u64, void*), void* ctx, + void* out, okld_u64 cap) { + if (!info || (info->count && !blocks) || !source) return 0; + const elf::u64 desc = prepared_fixed + static_cast(info->count) * 24; + const elf::u64 header = sizeof(elf::nhdr) + 8 + ((desc + 3) & ~3ull); + // Each block at an offset congruent to its address modulo the alignment. + elf::u64 at = header; + for (elf::u32 i = 0; i < info->count; ++i) { + const elf::u64 offset = at + ((blocks[i].address % alignment) + alignment - (at % alignment)) % alignment; + at = offset + blocks[i].size; + } + const elf::u64 total = at; + if (!out || cap < total) return total; + auto* o = static_cast(out); + fill(o, 0, total); + const elf::nhdr nh{ 8, static_cast(desc), prepared_note }; + copy(o, &nh, sizeof nh); + copy(o + sizeof nh, "OPENKAL", 8); + unsigned char* d = o + sizeof nh + 8; + put_info(d, *info); + at = header; + for (elf::u32 i = 0; i < info->count; ++i) { + const elf::u64 offset = at + ((blocks[i].address % alignment) + alignment - (at % alignment)) % alignment; + const okld_prepared_block b{ blocks[i].address, offset, blocks[i].size }; + copy(d + prepared_fixed + i * 24, &b, 24); + const void* bytes = source(blocks[i].address, blocks[i].size, ctx); + if (!bytes) return 0; + copy(o + offset, bytes, blocks[i].size); + at = offset + blocks[i].size; + } + return total; +} + +namespace { +// The stored bytes of the writable segment at an address of a base: the file's +// bytes, as they are written into a prepared copy before they are relocated +// there. +struct file_of { const view* v; const unsigned char* image; okld_u64 base; }; +const void* file_segment(okld_u64 address, okld_u64, void* c) { + auto* f = static_cast(c); + const elf::phdr* p = writable_segment(*f->v, address - f->base); + return p ? f->image + p->offset : nullptr; +} +} // namespace + +extern "C" okld_u64 okld_prepare(const void* image, okld_size n, okld_u64 base, const unsigned char digest[32], + void* out, okld_u64 cap, okld_why* asked) { + okld_why own{}; + okld_why* why = asked ? asked : &own; + view v; + if (!open(image, n, v, why->text, sizeof why->text)) return 0; + if (base == 0 || base % alignment != 0) { + sentence(why->text, sizeof why->text) << "a base that is not a multiple of 65536"; + return 0; + } + okld_prepared_info info{}; + info.version = 1; + info.base = base; + if (digest) copy(info.digest, digest, 32); + okld_prepared_block blocks[8]; + for (unsigned k = 0; k < v.eh->phnum && info.count < 8; ++k) { + const auto& ph = v.ph[k]; + if (!stored(ph)) continue; + if (ph.offset + ph.filesz > v.len) { + sentence(why->text, sizeof why->text) << "a segment extends beyond the file"; + return 0; + } + blocks[info.count++] = okld_prepared_block{ base + ph.vaddr, 0, ph.filesz }; + } + if (info.count == 0) { + sentence(why->text, sizeof why->text) << "the image has no writable segment with bytes in the file"; + return 0; + } + file_of f{ &v, static_cast(image), base }; + const okld_u64 total = okld_prepare_blocks(&info, blocks, &file_segment, &f, nullptr, 0); + if (!out || cap < total) return total; + // The segments written where they go, and then relocated there. + auto* o = static_cast(out); + if (okld_prepare_blocks(&info, blocks, &file_segment, &f, out, cap) != total) return 0; + okld_prepared_info back{}; + okld_prepared_block placed[8]; + if (okld_prepared(o, total, &back, placed, 8) != 0) return 0; + for (elf::u32 i = 0; i < back.count; ++i) { + const elf::phdr* p = writable_segment(v, placed[i].address - base); + if (p) apply_relative(v, base, o + placed[i].offset, p->vaddr, p->filesz); + } + return total; +} + +extern "C" int okld_prepared(const void* prepared, okld_size n, okld_prepared_info* info, okld_prepared_block* blocks, + okld_u32 cap) { + if (!prepared || !info || n < sizeof(elf::nhdr) + 8 + prepared_fixed) return -1; + const auto* b = static_cast(prepared); + elf::nhdr nh{}; + copy(&nh, b, sizeof nh); + if (nh.namesz != 8 || nh.type != prepared_note || !equal(reinterpret_cast(b + sizeof nh), "OPENKAL")) return -1; + if (nh.descsz < prepared_fixed || sizeof nh + 8 + nh.descsz > n) return -1; + const unsigned char* d = b + sizeof nh + 8; + get_info(d, *info); + if (info->version != 1 || prepared_fixed + static_cast(info->count) * 24 > nh.descsz) return -1; + for (elf::u32 i = 0; i < info->count && i < cap; ++i) { + copy(&blocks[i], d + prepared_fixed + i * 24, 24); + if (blocks[i].offset > n || blocks[i].size > n - blocks[i].offset) return -1; + } + return 0; +} diff --git a/ld/src/read.cpp b/ld/src/read.cpp index 7f1f2e3..ef03379 100644 --- a/ld/src/read.cpp +++ b/ld/src/read.cpp @@ -286,10 +286,10 @@ extern "C" int okld_check(const void* bytes, okld_size n, okld_name_test known_k } } - // A PLACEMENT, WHERE ONE IS STATED, DESCRIBES THE IMAGE AND HOLDS ITS - // SEGMENTS AS THE RELATIVE RELOCATIONS COMPUTE THEM FOR ITS BASE (SPEC - // 12.4, type 3): every word that differs from the file's is a relative - // relocation's, with the value it computes, and no other word differs. + // A PLACEMENT, WHERE ONE IS STATED, DESCRIBES THE IMAGE (SPEC 12.4, type + // 3): an entry for each writable segment with bytes in the file, its stored + // bytes within the file. What they hold is okld_check_placement's question, + // asked when an image is sealed and not on every load. elf::u32 psz = 0; if (const auto* pd = find_note(v, nullptr, placement_note, &psz); pd && psz >= 16) { elf::u32 count = 0; @@ -297,9 +297,20 @@ extern "C" int okld_check(const void* bytes, okld_size n, okld_name_test known_k placement p; if (count != 0 && !read_placement(v, p)) c.fail("its placement note does not describe its writable segments (SPEC 12.4)"); - else if (count != 0) verify_placement(v, p, c); } + // A TOUCH NOTE, WHERE ONE IS STATED, HOLDS WHAT IT COUNTS; A PREPARED COPY + // IS NO IMAGE (SPEC 12.4, types 4 and 5): a loader writes one for itself, + // and refuses an image that carries one. + elf::u32 tsz = 0; + if (const auto* td = find_note(v, nullptr, touch_note, &tsz); td && tsz >= 8) { + elf::u32 count = 0; + copy(&count, td, 4); + if (8 + static_cast(count) * 8 > tsz) c.fail("its touch note counts more ranges than it holds (SPEC 12.4)"); + } + if (find_note(v, nullptr, prepared_note, nullptr)) + c.fail("carries a loader's prepared copy (OPENKAL, type 5), which an image as distributed does not (SPEC 12.4)"); + elf::u32 sz = 0; const auto* req = find_note(v, nullptr, 1, &sz); if (!req || sz < 16) c.fail("has no requirements note (OPENKAL, type 1; SPEC 12.4)"); @@ -310,6 +321,16 @@ extern "C" int okld_check(const void* bytes, okld_size n, okld_name_test known_k return c.failures; } +extern "C" int okld_check_placement(const void* bytes, okld_size n, okld_report report, void* ctx) { + checker c{report, ctx}; + view v; + char why[160]; + if (!open(bytes, n, v, why, sizeof why)) { c.fail(why); return c.failures; } + placement p; + if (read_placement(v, p)) verify_placement(v, p, c); + return c.failures; +} + extern "C" int okld_set_requirements(void* bytes, okld_size n, okld_u64 interfaces, okld_why* why) { view v; if (!open(bytes, n, v, why->text, sizeof why->text)) return -1; diff --git a/run/build.mcpp b/run/build.mcpp index b59cb44..0f083fb 100644 --- a/run/build.mcpp +++ b/run/build.mcpp @@ -55,6 +55,45 @@ int main() { }; emit("surface.inc", main_names); emit("surface_space.inc", space_names); + + // A PERFECT HASH OF EVERY NAME (0.4): okrun_resolve is asked once for + // each name an image imports, and a comparison with every name in turn + // made it a pass over the surface for each. A seed is searched for under + // which no two names share a slot; the runner hashes a name once, reads + // its slot and compares one name. + std::vector all(main_names); + all.insert(all.end(), space_names.begin(), space_names.end()); + constexpr unsigned slots = 2048; // about ten times the names: a seed is found in thousands of trials + auto hash = [](unsigned seed, const std::string& n) { + unsigned h = 2166136261u ^ seed; + for (unsigned char c : n) { h ^= c; h *= 16777619u; } + return h % slots; + }; + unsigned seed = 0; + std::vector table; + for (unsigned trial = 1; trial < 1000000; ++trial) { + table.assign(slots, 0); + bool clean = true; + for (std::size_t i = 0; i < all.size() && clean; ++i) { + auto& slot = table[hash(trial, all[i])]; + if (slot) clean = false; else slot = static_cast(i + 1); + } + if (clean) { seed = trial; break; } + } + if (!seed) { + std::puts("mcpp:warning=no seed hashes the surface without a collision"); + return 1; + } + { + std::ofstream o(std::filesystem::path(out) / "surface_hash.inc"); + o << "// Generated by openkal-run's build.mcpp: a perfect hash of the surface's names.\n"; + o << "inline constexpr unsigned kResolveSeed = " << seed << "u;\n"; + o << "inline constexpr unsigned kResolveSlots = " << slots << "u;\n"; + o << "inline constexpr unsigned long kResolveMain = " << main_names.size() << "u;\n"; + o << "inline constexpr unsigned short kResolveSlot[" << slots << "] = {"; + for (unsigned i = 0; i < slots; ++i) o << (i % 16 == 0 ? "\n " : " ") << table[i] << ","; + o << "\n};\n"; + } std::printf("mcpp:include-dir=%s\n", out); std::printf("mcpp:rerun-if-changed=%s\n", surface.string().c_str()); return 0; diff --git a/run/src/bindings.cpp b/run/src/bindings.cpp index e5b3b37..fe37298 100644 --- a/run/src/bindings.cpp +++ b/run/src/bindings.cpp @@ -106,6 +106,7 @@ OKRUN_IMAGE_ABI kal_u64 own_kal_interfaces(void) { return kal_interfaces() | KAL_IFACE_PROF | KAL_IFACE_DEBUG; } OKRUN_IMAGE_ABI void own_kal_exit(int status) { + profile_phase("exit"); profile_finish(); kal_exit(status); } @@ -120,9 +121,44 @@ OKRUN_IMAGE_ABI int own_kal_prof_counter(const char* name, kal_uintptr len, cons OKRUN_IMAGE_ABI void own_kal_prof_mark(const char* name, kal_uintptr len, int begin) { profile_mark(name, len, begin); } +// AN ADDRESS NAMED: an image's from the symbol table of its file, which this +// runner read; any other --- the runner's own, the implementation's --- by the +// implementation (openkal 0.23). OKRUN_IMAGE_ABI kal_intptr own_kal_debug_symbolize(kal_uintptr address, char* out, kal_uintptr cap, kal_uintptr* start) { - return symbolize(address, out, cap, start); + const kal_intptr n = symbolize(address, out, cap, start); + return n >= 0 ? n : kal_debug_symbolize(address, out, cap, start); +} +// WHAT ENDS THE PROGRAM FOR A FAULT, AND WHAT SAMPLES AND TOUCHES, IS THE +// IMPLEMENTATION'S (openkal 0.23, SPEC 12.3): a fault, a timer and a page's +// first execution are the environment's to see, and this runner forwards. A +// measurement of this runner's own that samples or records touches holds them, +// and the program asking is told kal_err_again. +OKRUN_IMAGE_ABI kal_uintptr own_kal_debug_props(void) { return kal_debug_props(); } +OKRUN_IMAGE_ABI kal_uintptr own_kal_prof_props(void) { return kal_prof_props(); } +OKRUN_IMAGE_ABI int own_kal_prof_sample_begin(kal_u32 per_second) { + if (profile_samples()) return kal_err_again; + return kal_prof_sample_begin(per_second); +} +OKRUN_IMAGE_ABI kal_intptr own_kal_prof_sample_take(kal_prof_sample* out, kal_uintptr count) { + if (profile_samples()) return 0; + return kal_prof_sample_take(out, count); +} +OKRUN_IMAGE_ABI int own_kal_prof_sample_end(void) { + if (profile_samples()) return kal_err_invalid; + return kal_prof_sample_end(); +} +OKRUN_IMAGE_ABI int own_kal_prof_touch_begin(const void* at, kal_uintptr size) { + if (profile_touches()) return kal_err_again; + return kal_prof_touch_begin(at, size); +} +OKRUN_IMAGE_ABI kal_intptr own_kal_prof_touch_take(kal_uintptr* out, kal_uintptr count) { + if (profile_touches()) return 0; + return kal_prof_touch_take(out, count); +} +OKRUN_IMAGE_ABI int own_kal_prof_touch_end(void) { + if (profile_touches()) return kal_err_invalid; + return kal_prof_touch_end(); } #define OKRUN_BIND(n, i) { #n, reinterpret_cast(&present<&::n>::call) }, diff --git a/run/src/prof.cpp b/run/src/prof.cpp index ba437f6..413394e 100644 --- a/run/src/prof.cpp +++ b/run/src/prof.cpp @@ -16,6 +16,16 @@ // counters the counters and phases the program registers are kept // perfmap /tmp/perf-.map is written, where this system's profiler // (perf) reads the names of an anonymous region's addresses +// phases the moments of the start --- admitted, loaded, initialised, +// main returned --- with the time and the page faults to each +// (0.4) +// samples[=hz] where the program's processor time goes, sampled by the +// implementation (openkal 0.23) at hz a second (1000), each +// stack named from the images' symbol tables (0.4) +// touch the order in which the program first executes the pages of its +// images' instructions, recorded by the implementation, as file +// offsets: `okld touch' writes them into an image's touch note, +// and `okprof report --order' orders its functions by them (0.4) // // separated by commas, and `:' after them receives the report, // `okprof-.json' (the working directory otherwise). Unset, an image is bound @@ -57,7 +67,9 @@ void lock() { while (__atomic_exchange_n(&g_lock, 1, __ATOMIC_ACQUIRE)) kal_task void unlock() { __atomic_store_n(&g_lock, 0, __ATOMIC_RELEASE); } // --- what was asked for ------------------------------------------------------------ -enum : int { asked_active = 1, asked_calls = 2, asked_counters = 4, asked_perfmap = 8 }; +enum : int { asked_active = 1, asked_calls = 2, asked_counters = 4, asked_perfmap = 8, + asked_phases = 16, asked_samples = 32, asked_touch = 64 }; +kal_u32 g_sample_rate = 1000; int g_mode = -1; // -1: not read yet char g_dir[512] = {}; kal_u64 g_started = 0; @@ -69,6 +81,19 @@ void read_spec(const char* spec, kal_uintptr n) { if (contains(spec, n, "calls")) mode |= asked_calls; if (contains(spec, n, "counters")) mode |= asked_counters; if (contains(spec, n, "perfmap")) mode |= asked_perfmap; + if (contains(spec, n, "phases")) mode |= asked_phases; + if (contains(spec, n, "touch")) mode |= asked_touch; + // `samples', or `samples='. + for (kal_uintptr i = 0; i + 7 <= n; ++i) { + if ((i != 0 && spec[i - 1] != ',') || !same_text(spec + i, 7, "samples")) continue; + if (i + 7 < n && spec[i + 7] != ',' && spec[i + 7] != ':' && spec[i + 7] != '=') continue; + mode |= asked_samples; + if (i + 7 < n && spec[i + 7] == '=') { + kal_u32 hz = 0; + for (kal_uintptr k = i + 8; k < n && spec[k] >= '0' && spec[k] <= '9'; ++k) hz = hz * 10 + static_cast(spec[k] - '0'); + if (hz > 0 && hz <= 10000) g_sample_rate = hz; + } + } if (mode == asked_active) mode |= asked_calls | asked_counters; // a bare `1' for (kal_uintptr i = 0; i < n; ++i) if (spec[i] == ':') { copy_text(g_dir, sizeof g_dir, spec + i + 1, n - i - 1); break; } @@ -341,10 +366,130 @@ void write_perf_map(image_record& r, const void* bytes, kal_u64 size) { bool g_finished = false; +// --- the phases of a start (0.4) ------------------------------------------------------ +struct moment { const char* name; kal_u64 ns, faults; }; +moment g_moments[16]; +unsigned g_moment_count = 0; + +kal_u64 faults_now() { + kal_process self{}; + if (kal_process_self(&self) != kal_ok) return 0; + kal_program_info info{}; + info.self_size = sizeof info; + const int e = kal_process_info(self, KAL_PINFO_FAULTS, &info); + kal_process_close(self); + return e == kal_ok && (info.present & KAL_PINFO_FAULTS) ? info.faults_minor + info.faults_major : 0; +} + +// --- the touches of the images' instructions (0.4) -------------------------------------- +bool g_touching = false; + +int touch_module(void* base, const void* phdrs, okld_u32 phnum, const char*, void*) { + struct ph { kal_u32 type, flags; kal_u64 offset, vaddr, paddr, filesz, memsz, align; }; + const auto* p = static_cast(phdrs); + const kal_u64 g = kal_exec_granularity(); + for (okld_u32 k = 0; k < phnum; ++k) { + if (p[k].type != 1 || !(p[k].flags & 1)) continue; // PT_LOAD, PF_X + const kal_u64 lo = p[k].vaddr & ~(g - 1), hi = (p[k].vaddr + p[k].memsz + g - 1) & ~(g - 1); + if (kal_prof_touch_begin(static_cast(base) + lo, hi - lo) == kal_ok) g_touching = true; + } + return 0; +} + +// The file offset of an address of a loaded image, through its program headers. +bool file_offset(kal_uintptr address, const char*& image, kal_u64& offset) { + okld_module* m = okld_containing(reinterpret_cast(address)); + if (!m) return false; + struct find { okld_module* m; const char* name; kal_uintptr address; kal_u64 offset; bool found; }; + find f{ m, nullptr, address, 0, false }; + okld_iterate([](void* base, const void* phdrs, okld_u32 phnum, const char* name, void* c) -> int { + auto* f = static_cast(c); + if (base != okld_base(f->m)) return 0; + struct ph { kal_u32 type, flags; kal_u64 offset, vaddr, paddr, filesz, memsz, align; }; + const auto* p = static_cast(phdrs); + const kal_u64 at = f->address - reinterpret_cast(base); + for (okld_u32 k = 0; k < phnum; ++k) + if (p[k].type == 1 && at >= p[k].vaddr && at < p[k].vaddr + p[k].filesz) { + f->offset = p[k].offset + (at - p[k].vaddr); + f->name = name; + f->found = true; + return 1; + } + return 1; + }, &f); + image = f.name; + offset = f.offset; + return f.found; +} + +// --- samples, as stacks named and counted (0.4) ----------------------------------------- +struct stack_count { kal_u64 hash; kal_u32 count; kal_u32 text; }; // text: offset into the names +struct folded { + stack_count* rows = nullptr; kal_u64 cap = 0, used = 0; + out_buffer names; + bool grow() { + const kal_u64 ncap = cap ? cap * 2 : 4096; + auto* n = static_cast(kal_alloc(ncap * sizeof(stack_count), 16)); + if (!n) return false; + for (kal_u64 i = 0; i < ncap; ++i) n[i] = stack_count{}; + for (kal_u64 i = 0; i < cap; ++i) { + if (!rows[i].count) continue; + kal_u64 j = rows[i].hash & (ncap - 1); + while (n[j].count) j = (j + 1) & (ncap - 1); + n[j] = rows[i]; + } + if (rows) kal_free(rows, cap * sizeof(stack_count), 16); + rows = n; cap = ncap; + return true; + } + // The stack's row, counted once more; its text's offset names it. + kal_u32 add(const char* stack, kal_uintptr len) { + kal_u64 h = 1469598103934665603ull; + for (kal_uintptr i = 0; i < len; ++i) { h ^= static_cast(stack[i]); h *= 1099511628211ull; } + if ((used + 1) * 2 > cap && !grow()) return ~0u; + kal_u64 j = h & (cap - 1); + while (rows[j].count) { + if (rows[j].hash == h) { ++rows[j].count; return rows[j].text; } + j = (j + 1) & (cap - 1); + } + rows[j] = stack_count{ h, 1, static_cast(names.used) }; + names.put(stack, len); + names.put("\0", 1); + ++used; + return rows[j].text; + } +}; + +// A frame's name: the image's symbol, the implementation's, or the address. +void frame_name(kal_uintptr pc, bool innermost, out_buffer& into) { + char name[256]; + kal_uintptr start = 0; + const kal_uintptr at = innermost ? pc : pc - 1; + kal_intptr n = symbolize(at, name, sizeof name, &start); + if (n < 0) n = kal_debug_symbolize(at, name, sizeof name, &start); + if (n > 0) into.put(name, static_cast(n) < sizeof name ? static_cast(n) : sizeof name); + else { into.put("0x"); into.number(pc, 16); } +} + } // namespace bool profile_active() { return (mode() & asked_active) != 0; } bool profile_calls() { return (mode() & asked_calls) != 0; } +bool profile_samples() { return (mode() & asked_samples) != 0; } +bool profile_touches() { return (mode() & asked_touch) != 0; } + +void profile_phase(const char* name) { + if (!(mode() & asked_phases)) return; + lock(); + if (g_moment_count < 16) g_moments[g_moment_count++] = moment{ name, kal_time_monotonic() - g_started, faults_now() }; + unlock(); +} + +void profile_begin_run() { + const int m = mode(); + if (m & asked_samples) kal_prof_sample_begin(g_sample_rate); + if (m & asked_touch) okld_iterate(&touch_module, nullptr); +} void record_call(unsigned long index, kal_u64 ns) { if (index >= kProfiledNames) return; @@ -467,6 +612,8 @@ void profile_finish() { b.put(",\n \"id\": "); const kal_u64 id = own_id(); b.number(id); + b.put(",\n \"started_ns\": "); + b.number(g_started); b.put(",\n \"wall_ns\": "); b.number(kal_time_monotonic() - g_started); b.put(",\n \"calls\": ["); @@ -501,7 +648,112 @@ void profile_finish() { b.put(", \"ns\": "); b.number(g_phases[i].ns); b.put(" }"); } - b.put(first ? "]\n}\n" : "\n ]\n}\n"); + b.put(first ? "]" : "\n ]"); + // THE PHASES OF THE START (0.4). + b.put(",\n \"moments\": ["); + for (unsigned i = 0; i < g_moment_count; ++i) { + b.put(i ? ",\n { \"name\": " : "\n { \"name\": "); + b.quoted(g_moments[i].name); + b.put(", \"ns\": "); b.number(g_moments[i].ns); + b.put(", \"faults\": "); b.number(g_moments[i].faults); + b.put(" }"); + } + b.put(g_moment_count ? "\n ]" : "]"); + // SAMPLES, AS STACKS (0.4): the outermost frame first, `;' between, as + // a folded stack is written. + // A TIMELINE beside them: each sample's time, from the start, and its + // stack's place in the list --- the input of `okprof report --format=trace'. + if (mode() & asked_samples) { + kal_prof_sample_end(); + folded f; + static kal_prof_sample batch[256]; + struct when { kal_u64 ns; kal_u32 text; kal_u64 context; }; + constexpr kal_u64 kTimeline = 65536; + auto* timeline = static_cast(kal_alloc(kTimeline * sizeof(when), 16)); + kal_u64 moments = 0; + for (;;) { + const kal_intptr got = kal_prof_sample_take(batch, 256); + if (got <= 0) break; + for (kal_intptr i = 0; i < got; ++i) { + out_buffer one; + for (kal_u32 d = batch[i].depth; d-- > 0;) { + frame_name(batch[i].pc[d], d == 0, one); + if (d) one.put(";"); + } + const kal_u32 text = f.add(one.data, one.used); + one.release(); + if (timeline && moments < kTimeline && text != ~0u) + timeline[moments++] = when{ batch[i].time_ns > g_started ? batch[i].time_ns - g_started : 0, text, + batch[i].context }; + } + } + b.put(",\n \"rate\": "); b.number(g_sample_rate); + b.put(",\n \"stacks\": ["); + bool any = false; + kal_u64 index = 0; + for (kal_u64 i = 0; i < f.cap; ++i) { + if (!f.rows[i].count) continue; + b.put(any ? ",\n { \"stack\": " : "\n { \"stack\": "); + any = true; + b.quoted(f.names.data + f.rows[i].text); + b.put(", \"count\": "); b.number(f.rows[i].count); + b.put(" }"); + f.rows[i].hash = index++; // its place in the list, for the timeline + } + b.put(any ? "\n ]" : "]"); + // Each stack's text and place, ordered by the text, which a sample + // names: the order the texts were made in. + struct pair { kal_u32 text; kal_u32 place; }; + auto* order = static_cast(kal_alloc((f.used ? f.used : 1) * sizeof(pair), 16)); + kal_u64 n = 0; + for (kal_u64 i = 0; order && i < f.cap; ++i) + if (f.rows[i].count) order[n++] = pair{ f.rows[i].text, static_cast(f.rows[i].hash) }; + for (kal_u64 i = 1; i < n; ++i) { // an insertion sort: the stacks are few beside the samples + const pair x = order[i]; + kal_u64 j = i; + while (j > 0 && order[j - 1].text > x.text) { order[j] = order[j - 1]; --j; } + order[j] = x; + } + b.put(",\n \"timeline\": ["); + for (kal_u64 k = 0; timeline && order && k < moments; ++k) { + kal_u64 lo = 0, hi = n; + while (lo + 1 < hi) { const kal_u64 mid = (lo + hi) / 2; if (order[mid].text <= timeline[k].text) lo = mid; else hi = mid; } + const kal_u64 place = n ? order[lo].place : 0; + b.put(k ? ", [" : "["); + b.number(timeline[k].ns); b.put(", "); b.number(place); b.put(", "); b.number(timeline[k].context); + b.put("]"); + } + b.put("]"); + if (order) kal_free(order, (f.used ? f.used : 1) * sizeof(pair), 16); + if (timeline) kal_free(timeline, kTimeline * sizeof(when), 16); + if (f.rows) kal_free(f.rows, f.cap * sizeof(stack_count), 16); + f.names.release(); + } + // TOUCHES (0.4): each image's pages in the order the program first executed + // them, as offsets in its file. + if ((mode() & asked_touch) && g_touching) { + static kal_uintptr pages[4096]; + b.put(",\n \"page\": "); b.number(kal_exec_granularity()); + b.put(",\n \"touches\": ["); + bool any = false; + for (;;) { + const kal_intptr got = kal_prof_touch_take(pages, 4096); + if (got <= 0) break; + for (kal_intptr i = 0; i < got; ++i) { + const char* image = nullptr; + kal_u64 offset = 0; + if (!file_offset(pages[i], image, offset)) continue; + b.put(any ? ",\n { \"image\": " : "\n { \"image\": "); + any = true; + b.quoted(image ? image : ""); + b.put(", \"offset\": "); b.number(offset); + b.put(" }"); + } + } + kal_prof_touch_end(); + b.put(any ? "\n ]" : "]"); + } + b.put("\n}\n"); char path[700]; kal_uintptr at = 0; if (g_dir[0]) { copy_text(path, sizeof path - 64, g_dir, length(g_dir)); at = length(path); path[at++] = '/'; } diff --git a/run/src/run.cpp b/run/src/run.cpp index d59121d..840fff7 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -266,6 +266,12 @@ struct search { // read for an image another needs until the loader gives it back. file_bytes ran{}; file_bytes needed[16] = {}; + // The prepared copy of the entry image for this boot's base (0.4), held + // while the loader maps its blocks. + file_bytes prepared{}; + // The member of the archive last admitted: the entry, when it is run (its + // digest is the manifest's). + char member[512] = {}; // Where each was read from, for a measurement and openkal.debug (0.2): // the file run (empty for bytes a program carries), and each needed file's // path and the name it was needed by. @@ -277,6 +283,7 @@ struct search { // The file a run's image or okb was read from, given back once the image is // loaded: its pages are no longer resident beside the image's own. void release_ran(search& s) { + if (s.prepared.data) release_file(s.prepared); if (!s.ran.data) return; if (s.archive == s.ran.data) { s.archive = nullptr; s.archive_size = 0; s.json = nullptr; s.json_size = 0; } release_file(s.ran); @@ -347,8 +354,51 @@ char* put_number(char* o, unsigned long long v) { return o; } -// The user's cache directory, as this system places it, then `okrun/admitted'. -bool memo_directory(kal_dir& out) { +// A directory of components beneath a supplied one, each made in turn and one +// that exists entered: `rest' (a path's remainder beneath `root') and then each +// of `below'. +bool enter_path(kal_dir root, const char* rest, const char* const* below, unsigned below_count, kal_dir& out, + bool* owned = nullptr) { + kal_dir d = root; + char part[1100]; unsigned long p = 0; + auto enter_part = [&]() -> bool { + if (p == 0) return true; + kal_dir next{}; + kal_fs_mkdir(d, part, p); + if (kal_fs_open_dir(d, part, p, &next) != kal_ok) return false; + if (!(d.h == root.h)) kal_fs_close_dir(d); + d = next; + p = 0; + return true; + }; + for (const char* c = rest ? rest : ""; ; ++c) { + if (*c == 0 || separator(*c)) { if (!enter_part()) return false; if (*c == 0) break; continue; } + if (p + 1 < sizeof part) part[p++] = *c; + } + for (unsigned i = 0; i < below_count; ++i) { + p = 0; + for (const char* c = below[i]; *c && p + 1 < sizeof part; ++c) part[p++] = *c; + if (!enter_part()) return false; + } + out = d; + if (owned) *owned = !(d.h == root.h); + return true; +} + +// A directory named by a variable, entered (and made), then `below' in it. +bool directory_of_variable(const char* var, const char* const* below, unsigned below_count, kal_dir& out, + bool* owned = nullptr) { + char path[1024]; + const kal_intptr n = kal_env_var(var, length(var), path, sizeof path - 1); + if (n <= 0 || n >= static_cast(sizeof path)) return false; + path[n] = 0; + kal_dir root{}; + const char* rest = nullptr; + return locate(path, root, rest) && enter_path(root, rest, below, below_count, out, owned); +} + +// The user's cache directory, as this system places it, then `below'. +bool user_cache(const char* const* below, unsigned below_count, kal_dir& out, bool* owned = nullptr) { char system[16]; const kal_intptr sn = kal_env_system(system, sizeof system); const bool windows = sn == 7 && same_bytes(system, "windows", 7); @@ -370,32 +420,13 @@ bool memo_directory(kal_dir& out) { path[at] = 0; kal_dir root{}; const char* rest = nullptr; - if (!locate(path, root, rest)) return false; - // Each component made in turn; one that exists is entered. - kal_dir d = root; - char part[1100]; unsigned long p = 0; - auto enter_part = [&]() -> bool { - if (p == 0) return true; - kal_dir next{}; - kal_fs_mkdir(d, part, p); - if (kal_fs_open_dir(d, part, p, &next) != kal_ok) return false; - if (!(d.h == root.h)) kal_fs_close_dir(d); - d = next; - p = 0; - return true; - }; - for (const char* c = rest; ; ++c) { - if (*c == 0 || separator(*c)) { if (!enter_part()) return false; if (*c == 0) break; continue; } - if (p + 1 < sizeof part) part[p++] = *c; - } + return locate(path, root, rest) && enter_path(root, rest, below, below_count, out, owned); +} + +// `okrun/admitted' in the user's cache directory. +bool memo_directory(kal_dir& out) { static const char* const below[] = { "okrun", "admitted" }; - for (const char* c : below) { - p = 0; - while (c[p]) { part[p] = c[p]; ++p; } - if (!enter_part()) return false; - } - out = d; - return true; + return user_cache(below, 2, out); } void memo_open(search& s, memo_place& m) { @@ -463,6 +494,7 @@ const void* archive_member(search& s, const char* member, unsigned long long& si okld_zip_member m{}; okld_why why{}; if (okld_zip_find(s.archive, s.archive_size, member, &m, &why) != 0) return nullptr; + copy_text(s.member, sizeof s.member, member); if (!g_memo_opened) { g_memo_opened = true; memo_open(s, g_memo); } if (memo_holds(g_memo, member)) { // Compared before, on this file as it is: its shape alone is asked, @@ -606,21 +638,51 @@ bool part_of(const file_bytes& f, const void* bytes, okld_size from, kal_u64& at return at % KAL_MAP_ALIGNMENT == 0; } -int map_part(void* region, okld_size offset, okld_size size, const void* bytes, okld_size from, int execute, +int map_part(void* region, okld_size offset, okld_size size, const void* bytes, okld_size from, int asked, void* ctx) { if (!(kal_exec_props() & KAL_EXEC_PROP_FILE)) return -1; auto& s = *static_cast(ctx); - const kal_uintptr how = execute ? KAL_EXEC_MAP_EXECUTE : 0; + const kal_uintptr how = ((asked & OKLD_MAP_EXECUTE) ? KAL_EXEC_MAP_EXECUTE : 0) + | ((asked & OKLD_MAP_PREFETCH) ? KAL_EXEC_MAP_PREFETCH : 0) + | ((asked & OKLD_MAP_WRITE) ? KAL_EXEC_MAP_WRITE : 0); kal_u64 at = 0; if (part_of(s.ran, bytes, from, at)) return kal_exec_map_part(region, offset, size, s.ran.file, at, how) == kal_ok ? 0 : -1; + if (part_of(s.prepared, bytes, from, at)) + return kal_exec_map_part(region, offset, size, s.prepared.file, at, how) == kal_ok ? 0 : -1; for (const auto& f : s.needed) if (part_of(f, bytes, from, at)) return kal_exec_map_part(region, offset, size, f.file, at, how) == kal_ok ? 0 : -1; return -1; } -void prepare_host(search& s) { +// Read-only after relocation (openkal 0.23), for good where the module is never +// unloaded. +int seal(void* region, okld_size offset, okld_size size, int forever, void*) { + return kal_exec_seal_part(region, offset, size, forever ? KAL_EXEC_SEAL_FOREVER : 0) == kal_ok ? 0 : -1; +} + +// WHETHER THE PAGES A START TOUCHES ARE READ IN AHEAD OF IT (0.4): memory spent +// for time, as the program's resource preference says (openkal 0.23) --- always +// for performance, never for economy, and where memory is plentiful (a quarter +// of it available) when balanced. +bool prefetching() { + switch (kal_env_resources()) { + case KAL_RESOURCES_PERFORMANCE: return true; + case KAL_RESOURCES_ECONOMY: return false; + default: { + kal_memory_status m{}; + m.self_size = sizeof m; + if (kal_memory_info(&m) != kal_ok || !(m.present & KAL_MSTATUS_AVAILABLE) || !(m.present & KAL_MSTATUS_TOTAL)) + return false; + return m.available_bytes >= m.total_bytes / 4; + } + } +} + +// `permanent': the modules loaded are never unloaded (a run's entry image and +// those it needs), as against a library a program closes. +void prepare_host(search& s, bool permanent) { okld_host& host = s.host; host.reserve = &reserve; host.publish = &publish; host.release = &release; host.map = &map_part; host.granularity = &granularity; host.resolve = &resolve; @@ -629,6 +691,245 @@ void prepare_host(search& s) { host.reserve_at = placing() ? &reserve_at : nullptr; host.debugger = s.options ? s.options->debugger : 0; host.ctx = &s; + host.seal = &seal; + host.permanent = permanent ? 1 : 0; + host.prefetch = prefetching() ? 1 : 0; +} + + +// --- this machine's base for an image (0.4) ------------------------------------------ +// +// AN IMAGE AT THE BASE IT WAS SEALED WITH IS AT ONE ADDRESS ON EVERY MACHINE, +// which is what a program linked for one address is and what address space +// layout randomisation exists to undo. So a runner moves it: by a slide chosen +// at random once for each user and each boot, as this system's own shared +// libraries are moved, from the image's base of its digest. The first start of +// the boot computes the image's writable segments for that base --- the +// relocations of the sealed placement, applied for another base --- and writes +// them as a prepared copy (SPEC 12.4, type 5) beside the slide; every later +// start maps it. The copy is the user's, in a directory only the user writes; +// a party that could rewrite it could as well debug the user's programs. +// +// OKRUN_PLACE=sealed the base the image was sealed with +// OKRUN_PLACE=0 anywhere, every relocation applied +// OKRUN_CACHE= the directory (else $XDG_RUNTIME_DIR/okrun, which this +// system empties at each boot, else the user's cache +// directory's okrun/placed) + +enum class policy { local, sealed, anywhere }; + +policy placement_policy() { + char v[16]; + const kal_intptr n = kal_env_var("OKRUN_PLACE", 11, v, sizeof v); + if (n == 1 && v[0] == '0') return policy::anywhere; + if (n == 6 && same_bytes(v, "sealed", 6)) return policy::sealed; + return policy::local; +} + +struct owned_dir { + kal_dir d{}; + bool own = false; + ~owned_dir() { if (own) kal_fs_close_dir(d); } +}; + +bool cache_directory(owned_dir& out) { + static const char* const okrun_dir[] = { "okrun" }; + static const char* const placed[] = { "okrun", "placed" }; + return directory_of_variable("OKRUN_CACHE", nullptr, 0, out.d, &out.own) + || directory_of_variable("XDG_RUNTIME_DIR", okrun_dir, 1, out.d, &out.own) + || user_cache(placed, 2, out.d, &out.own); +} + +// A small file of the directory, read whole. +kal_intptr read_small(kal_dir dir, const char* name, char* out, unsigned long cap) { + kal_file f{}; + if (kal_fs_open(dir, name, length(name), KAL_OPEN_READ, &f) != kal_ok) return -1; + const kal_intptr n = kal_stream_read(kal_fs_stream(f), out, cap - 1); + kal_fs_close_file(f); + if (n >= 0) out[n] = 0; + return n; +} + +// A file of the directory written whole, then put in place by one rename, so +// that a reader finds the old one or the new one and never a part. +bool write_whole(kal_dir dir, const char* name, const void* bytes, unsigned long long n) { + char temp[256]; + { + text t(temp, sizeof temp); + unsigned char r[8] = {}; + kal_random_fill(r, sizeof r); + char h[17]; + to_hex(r, 8, h); + t << "." << name << "." << h << ".tmp"; + } + kal_file f{}; + if (kal_fs_open(dir, temp, length(temp), KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &f) != kal_ok) + return false; + const auto* b = static_cast(bytes); + unsigned long long done = 0; + while (done < n) { + const kal_intptr w = kal_stream_write(kal_fs_stream(f), b + done, n - done); + if (w <= 0) break; + done += static_cast(w); + } + kal_fs_close_file(f); + if (done != n || kal_fs_rename(dir, temp, length(temp), dir, name, length(name)) != kal_ok) { + kal_fs_remove(dir, temp, length(temp)); + return false; + } + return true; +} + +unsigned long long parse_number(const char*& p) { + unsigned long long v = 0; + while (*p >= '0' && *p <= '9') v = v * 10 + static_cast(*p++ - '0'); + return v; +} + +// THE SLIDE OF THIS BOOT: a multiple of 64 KiB below 16 TiB, kept with the wall +// time the boot began at (the wall clock less the monotonic one, which begins +// at the boot). Another boot has another beginning, and its first start chooses +// again and removes what was prepared for the last. +bool boot_slide(kal_dir dir, unsigned long long& slide) { + const unsigned long long began = kal_time_wall() - kal_time_monotonic(); + char have[128]; + if (read_small(dir, "boot", have, sizeof have) > 0) { + const char* p = have; + const unsigned long long s0 = parse_number(p); + if (*p == ' ') ++p; + const unsigned long long b0 = parse_number(p); + const unsigned long long apart = b0 > began ? b0 - began : began - b0; + if (apart <= 2000000000ull && s0 % 65536 == 0 && s0 < (1ull << 44)) { slide = s0; return true; } + } + unsigned long long r = 0; + if (kal_random_fill(&r, sizeof r) != kal_ok) return false; + slide = (r % (1ull << 28)) << 16; + // What was prepared for another boot is for another slide. + kal_uintptr iter = 0; + if (kal_fs_list_begin(dir, &iter) == kal_ok) { + while (iter != 0) { + char name[256]; kal_uintptr n = 0; int kind = 0; + if (kal_fs_list_next(dir, &iter, name, sizeof name, &n, &kind) != kal_ok || iter == 0) break; + if (n > 6 && n < sizeof name && same_bytes(name + n - 6, ".okseg", 6)) kal_fs_remove(dir, name, n); + if (n > 7 && n < sizeof name && same_bytes(name + n - 7, ".oksnap", 7)) kal_fs_remove(dir, name, n); + } + } + char line[96]; + char* o = put_number(line, slide); + *o++ = ' '; + o = put_number(o, began); + *o++ = '\n'; + write_whole(dir, "boot", line, static_cast(o - line)); + return true; +} + +// THE IMAGE'S DIGEST, its name in the cache: an okb member's is the one its +// manifest states, which its admission held it to; a file's is computed once +// and remembered by the file's identity, length and time, as an admission is. +bool image_digest(search& s, const void* bytes, unsigned long long size, unsigned char out[32]) { + const auto* b = static_cast(bytes); + if (s.json && s.archive && b >= s.archive && b < s.archive + s.archive_size && s.member[0]) + return okld_okb_digest(s.json, s.json_size, s.member, out) == 1; + if (!s.ran_path[0] || !s.ran.data || b < s.ran.data || b >= s.ran.data + s.ran.size) return false; + kal_dir dir{}; + const char* rest = nullptr; + if (!locate(s.ran_path, dir, rest)) return false; + kal_node_info info{}; + info.self_size = sizeof info; + if (kal_fs_info(dir, rest, length(rest), 0, KAL_INFO_IDENTITY | KAL_INFO_SIZE | KAL_INFO_MODIFIED, &info) != kal_ok + || !(info.present & KAL_INFO_IDENTITY) || !(info.present & KAL_INFO_MODIFIED)) { + okld_sha256(bytes, size, out); + return true; + } + char stamp[128]; + char* o = stamp; + o = put_number(o, info.identity[0]); *o++ = ' '; + o = put_number(o, info.identity[1]); *o++ = ' '; + o = put_number(o, info.size); *o++ = ' '; + o = put_number(o, info.modified_ns); *o++ = ' '; + *o = 0; + const unsigned long stamp_len = static_cast(o - stamp); + static const char* const below[] = { "okrun", "digests" }; + owned_dir memo; + char name[40]; + { + unsigned char k[32]; + okld_sha256(s.ran_path, length(s.ran_path), k); + to_hex(k, 16, name); + } + if (user_cache(below, 2, memo.d, &memo.own)) { + char have[256]; + const kal_intptr n = read_small(memo.d, name, have, sizeof have); + if (n == static_cast(stamp_len + 64) && same_bytes(have, stamp, stamp_len)) { + for (int i = 0; i < 32; ++i) { + auto nib = [](char c) { return c >= 'a' ? c - 'a' + 10 : c - '0'; }; + out[i] = static_cast(nib(have[stamp_len + 2 * i]) * 16 + nib(have[stamp_len + 2 * i + 1])); + } + return true; + } + } + okld_sha256(bytes, size, out); + if (memo.d.h) { + char line[256]; + unsigned long at = 0; + for (unsigned long i = 0; i < stamp_len; ++i) line[at++] = stamp[i]; + to_hex(out, 32, line + at); + write_whole(memo.d, name, line, at + 64); + } + return true; +} + +// The prepared copy of the entry image for this boot's base, found or made, +// mapped into `s.prepared'. False leaves the image to its sealed base. +bool prepare_local(search& s, const void* bytes, unsigned long long size) { + if (placement_policy() != policy::local || !s.host.reserve_at) return false; + okld_u64 sealed = 0; okld_u32 count = 0; + if (okld_placement(bytes, size, &sealed, &count) != 1 || count == 0) return false; + unsigned char digest[32]; + if (!image_digest(s, bytes, size, digest)) return false; + owned_dir dir; + if (!cache_directory(dir)) return false; + unsigned long long slide = 0; + if (!boot_slide(dir.d, slide)) return false; + // From the digest's own base, as okld_place_base chooses one, moved by the + // slide within [16 TiB, 32 TiB - 4 GiB): room for a span of 4 GiB above. + const unsigned long long slot = (static_cast(digest[0]) | static_cast(digest[1]) << 8) & 0xfff; + const unsigned long long range = (1ull << 44) - (1ull << 32); + const unsigned long long base = (1ull << 44) + (slot * (1ull << 32) + slide) % range; + char name[96]; + { + char h[33], b[17]; + to_hex(digest, 16, h); + char* o = b; + for (int sh = 60; sh >= 0; sh -= 4) *o++ = hex_digits[(base >> sh) & 15]; + *o = 0; + text(name, sizeof name) << h << "-" << b << ".okseg"; + } + auto found = [&]() -> bool { + kal_file f{}; + if (kal_fs_open(dir.d, name, length(name), KAL_OPEN_READ, &f) != kal_ok) return false; + file_bytes fb; + if (!map_all(f, fb)) { kal_fs_close_file(f); return false; } + okld_prepared_info info{}; + okld_prepared_block blocks[1]; + if (okld_prepared(fb.data, fb.size, &info, blocks, 0) != 0 || info.base != base || info.flags != 0 + || !same_bytes(info.digest, digest, 32)) { + release_file(fb); + return false; + } + s.prepared = fb; + return true; + }; + if (found()) return true; + okld_why why{}; + const okld_u64 need = okld_prepare(bytes, size, base, digest, nullptr, 0, &why); + if (need == 0) return false; + void* buffer = kal_alloc(need, 16); + if (!buffer) return false; + const bool made = okld_prepare(bytes, size, base, digest, buffer, need, &why) == need + && write_whole(dir.d, name, buffer, need); + kal_free(buffer, need, 16); + return made && found(); } using image_function = void (OKRUN_IMAGE_ABI*)(void); @@ -645,10 +946,19 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam state().name = name; state().carried = s.options && s.options->carried; state().started = true; - prepare_host(s); + prepare_host(s, true); + profile_phase("admitted"); okld_module* m = nullptr; okld_why reason{}; - if (okld_load(bytes, size, name, &s.host, &m, &reason) != 0) { + // AT THIS BOOT'S BASE WHERE THE RUNNER PREPARED IT, at the base it was + // sealed with otherwise, and anywhere where neither can be had (0.4). + int loaded = -1; + if (prepare_local(s, bytes, size)) { + loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason); + if (loaded != 0) { release_file(s.prepared); reason = okld_why{}; } + } + if (loaded != 0) loaded = okld_load(bytes, size, name, &s.host, &m, &reason); + if (loaded != 0) { if (s.refusal[0]) text(why, cap) << name << ": " << s.refusal; else text(why, cap) << name << ": " << reason.text; return -1; @@ -664,10 +974,14 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam profile_loaded(name, bytes, size, fromRan && s.ran_path[0] ? s.ran_path : nullptr, fromRan ? static_cast(b - s.ran.data) : 0); } + profile_phase("loaded"); profile_settle(); release_ran(s); + profile_begin_run(); okld_initialise(m, &call_image, nullptr); + profile_phase("initialised"); const int status = reinterpret_cast(entry)(); + profile_phase("main"); okld_finalise(m, &call_image, nullptr); profile_finish(); kal_exit(status); @@ -772,13 +1086,22 @@ extern "C" const char* const* okrun_names(unsigned long* count) { return names; } +// A NAME'S SLOT, from a hash under which no two of the surface's names share +// one (build.mcpp, 0.4): one name compared, where every name was. +#include "surface_hash.inc" + extern "C" void* okrun_resolve(const char* name) { + if (!name) return nullptr; + unsigned h = 2166136261u ^ kResolveSeed; + for (const char* c = name; *c; ++c) { h ^= static_cast(*c); h *= 16777619u; } + const unsigned short slot = kResolveSlot[h % kResolveSlots]; + if (slot == 0) return nullptr; unsigned long n = 0; - const binding* t = bindings(n); - for (unsigned long i = 0; i < n; ++i) if (same(t[i].name, name)) return t[i].address; - t = space_bindings(n); - for (unsigned long i = 0; t && i < n; ++i) if (same(t[i].name, name)) return t[i].address; - return nullptr; + const unsigned long i = slot - 1u; + const binding* t = i < kResolveMain ? bindings(n) : space_bindings(n); + const unsigned long k = i < kResolveMain ? i : i - kResolveMain; + if (!t || k >= n || !same(t[k].name, name)) return nullptr; + return t[k].address; } extern "C" int okrun_run_bytes(const void* bytes, unsigned long long size, const char* name, @@ -850,7 +1173,7 @@ namespace { okrun_library* open_loaded(okrun_library* lib, const void* bytes, unsigned long long size, const char* name, char* why, unsigned long long cap) { if (!admit(bytes, size, name, lib->s.options, why, cap)) return nullptr; - prepare_host(lib->s); + prepare_host(lib->s, false); okld_why reason{}; if (okld_load(bytes, size, name, &lib->s.host, &lib->module, &reason) != 0) { if (lib->s.refusal[0]) text(why, cap) << name << ": " << lib->s.refusal; diff --git a/run/src/runner.h b/run/src/runner.h index 3b1e690..77e9cd6 100644 --- a/run/src/runner.h +++ b/run/src/runner.h @@ -28,6 +28,15 @@ const binding* bindings(unsigned long& count); inline constexpr unsigned long kProfiledNames = 256; bool profile_active(); bool profile_calls(); +bool profile_samples(); // `samples': this runner takes them (0.4) +bool profile_touches(); // `touch': this runner records them (0.4) +// The phases of a start (0.4, `phases'): each mark is a moment of the start --- +// admitted, loaded, relocated, initialised, main returned --- with the time and +// the page faults so far. +void profile_phase(const char* name); +// What a measurement takes once the image is loaded and before it runs: the +// samples begun, the touches of its executable parts begun (`okld_iterate'). +void profile_begin_run(); void record_call(unsigned long index, kal_u64 ns); int profile_counter(const char* name, kal_uintptr len, const volatile kal_u64* cell); void profile_mark(const char* name, kal_uintptr len, int begin); diff --git a/tools/scripts/__pycache__/okgdb_common.cpython-312.pyc b/tools/scripts/__pycache__/okgdb_common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..601a7b5df76e998f57a065a7fffee7ec66b6eda5 GIT binary patch literal 5465 zcmbVQTTmO<89poRu5JiP$bfIe>p+6#*eEs^+a#go?pttd$BE52A&Yhq0-;rQ7Xiv@ zOgnuLF)4W387nh+P?|g_b*DpSIt^(bYNvhZOkcFf3CfnY=7pI)5ZtM!ZfDZ}tZpEa z@w7*yv*-Sw%YVNA`_EtOb}NEFk?Uh$RUq^?oGAoDKJnlrByJ-PaYPUe=Dx%r!5Myn z1`V7MVk1XFOmY;&6h}i$b0&yQGla*yj)xiW5&6;Z#{z#N{2%-sN^c{%Sf_xDJ9)XS zC`Ks67;aYmj>dV>=0`r<0)B;Y33M6BRAI<5 zx{xY;ITMUxzLS@H9c0CfPv~KR9@cF=NSHfbXIW5;g}D>xdsb&v$SBOO&@M)m&)<(? zv}`L3e1j~s>_Kn*0Oo3X!dz`PD9);wbQy&;nxUULW{Fv2wpc~X9;=Kw6f;*behBuI zm_MS>WA=i)(pRx6HpM=&WkvIyBvRz9p#yz zz;f(xkY^;`Kf#A&i4jH^HpKLv?_)*+LB5?iB#ewmynKX-upB2I(MzSMr%5x2!|OkU zISj0m5I?}t4d}LxwdRrG#f$4qE^&GttL-Dpi)YSU)WbT~8IMULg)kq|NRekbjh19_ z(kE-S$xxW}jSr28!h}XjJU^~cA{+Aa8qJ2GIp=X|WSEslHDW{~M{!pwJPK39pU~ul zM#*?Nno*dPH6j3|@=O>y4~+y_zogN*#b}hi7$^?oMQJo%suYF%1V5pF*m3y)c&`-Z*>UfbAdX8gW8ORA3x|V&VabbEHO>Z)1}0cP@0A51D0wBH7zoQy zH}2<#hkU}sgb->9&uHetwu_A*$^?JO0=qMdzOmTj++t70vS$OCsQSdkti!nwnTsqA z-;Bn|tj)1#SaiitE=E^u2a~U+4u1Ih67h@cS$FkA6Ey!8_xXnzRqbAN)vK=hWKXJL z$&hh1&(U#e%~iWYY1V96prLwl-kx>3Zqv8uMKRHPE1GdW9j6{TV7v_{ve{?5*X&h` ztv9c4A|v&DoLK9BdA0wV+J7xQ$fy0mbSRvbWHlsb`X^O$Pn?QJ79)wtlile**jL}ETX|7#M>7#;(~f965a z5j}-Lv47wSGdvjxaz_JP86F;C0wHFUpJqAU2Nyhegb_q$KnU?oA_l+#5g(|CO}U7t zE`ljXB1UWxo40kGjgo+bXVx*`um9(}@4j1iKuW?{kn5pEoE0(hh*Dje;CNHc<5 zk6Wz41+}<<sdrI>#W2KG&%(S$xw)CkjeRtnUzj`%2IFugd(*99(cvKx6 z&9nqm^D)30aj`9NJRv6AQpZ!Gjy3NB*6hmK-El*<(z$qi&Y!4BKKuTGOy!}qhQq53 zZE8c?Qe;`adnwa!TCF%3?_2Ck^a6?|47W}sA}QkiXzG&slvi~>AMeRJsuQkv-$}^H zu2j{B{i&{{s!#gU2U?ctl>;4jTmRhoMSHe(U!o_uD@A`;m#+1$);6oP&6(QP6=!SK zRh@V?dF=heDTBJ_u63g!Sk>SC8BH zCpXoze>?bLrNWx?U>Ty@K#-c_t*t zP;%(3{O@mURgj{pzjo}}fGbh`NWy}CT+wMq zKlOAH#?LH}x<^o*R?|J&h|^X*wp%*)81L;SAl}*f{T8m^cI#I-DooxbgM!I+p3oyS zRy+=no)2Sq8_R-HB)@)QOK-f_Wnm|z=D;;6sK-vUb<2pAWy3+XF#jI3VDK5R`|t+L z5IY(`glWZa%P?;tp)_XDEsaqz0_}bUx7af7PV2<5S{A@E8x?{B$7wG(101D0GBh6d zPJViG&CN3aiPO@O|#u!+bXjTcigsC>0I#7`4@yaA;D!T>(lmz-yHv}{nPf(IzH`4cU?|j zc|Fs7b+!3TwfW8aL+t8MKphHXn#aCs45!$Yknl@lr7^79!?S&03NUf41B6Lx#J;TvikGh=^(Z8EzNnzS4G3L_ zRxx9Yj8TeF0mYv-U~(o}AV*<^9EVE`>QoZ~1)ATZ5DJNjU=%0^uEpPint{)6h@Qo& zr7KM%s0%Cc;K6}!#js466=h}udsQ66%FJ65a0yU1y;j%7oUa5bsXKs;O#>c6cT<3) zU=9KrfqUe5h7}x{p;%7_tBf1 z$UwcInh(aUi@k}qtgSjxbKlmQJe-QCPq%($Yt6cMCwh~wrN-1}POAG)sqXfbiuMf( zI&IK!5)WW3v~AO|p6i?bmX3}F1Y1Rv9yVft8KP~A$t*h15;0?taZf$KV@UDQ0v=-`6m}QG+V1YU_KHQSO0lnc- zC1$-rf$MKAJwcQnHE?^tGTo?yy6lE_^!Rl+7`u$=s2&u+I;Hqn60$hyx2U$YNxOZyV7*0(KFnVJr`UX~dm-UkwZ=C*y@ouWQE ztL|$`JG@J+8OO=&iI?u~T{&^?=TpFOucZ3!*EXjwzJ|%RZz3IzsJ@6gIHg{kzVDp= zmcm1AP+0zk6%$s+cEb9xtE<8IMFZ7!)Z{UXov`4XH`9ny*l*BDmX7@TzU!Ow2@)^i zD(q%FGQT(>aFap4LmY%G++QR;2NAF)f_Ok9!unTa-=rOcW3$0UI5!W`MD=Es6;gXC N!uh~yC!G3V{{<6J)FS`@ literal 0 HcmV?d00001 diff --git a/tools/scripts/okgdb.py b/tools/scripts/okgdb.py new file mode 100644 index 0000000..39c0f93 --- /dev/null +++ b/tools/scripts/okgdb.py @@ -0,0 +1,62 @@ +# okgdb.py --- the symbols of the ok images a program has mapped, for gdb. +# +# (gdb) source okgdb.py +# (gdb) ok-symbols # a running program, or a core file +# +# An image's instructions and read-only data are the pages of its file +# (openkal 0.21, kal_exec_map_part), so the program's mappings --- and a core +# file's, which records them --- name each image and where it lies: the +# address of a part, less the part's address in the image, is the image's +# base. Each image is given to gdb with its symbols at that base; where +# `okld debug-split' took its debug information away, the debug file is found +# by the image's build-id beside the image (`.build-id/xx/rest.debug') or in +# OKGDB_DEBUG_DIR, as debuginfod lays one out. +# +# An image the runner copied rather than mapped (a system without +# KAL_EXEC_PROP_FILE, or `okrun --debugger') is announced through the GDB JIT +# interface instead, which gdb reads without this file. +import os +import re +import sys + +import gdb + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import okgdb_common as common # noqa: E402 + + +def _mappings(): + """(start, file offset, path) of each mapping of a file, from gdb's own report.""" + text = gdb.execute("info proc mappings", to_string=True) + out = [] + for line in text.splitlines(): + m = re.match(r"\s*(0x[0-9a-f]+)\s+(0x[0-9a-f]+)\s+(0x[0-9a-f]+)\s+(0x[0-9a-f]+)\s+(?:\S+\s+)?(/.*)$", line) + if m: + out.append((int(m.group(1), 16), int(m.group(4), 16), m.group(5).strip())) + return out + + +class OkSymbols(gdb.Command): + """Load the symbols of every ok image the program has mapped, at its base.""" + + def __init__(self): + super().__init__("ok-symbols", gdb.COMMAND_FILES) + + def invoke(self, arg, from_tty): + bases = {} + for start, offset, path in _mappings(): + if path in bases or not os.path.exists(path) or not common.is_image(path): + continue + base = common.base_at(path, start, offset) + if base is not None: + bases[path] = base + if not bases: + print("ok-symbols: no ok image is mapped (one the runner copied is announced through the JIT interface)") + return + for path, base in sorted(bases.items()): + symbols = common.debug_file(path) + gdb.execute("add-symbol-file %s -o 0x%x" % (symbols, base), to_string=True) + print("ok-symbols: %s at 0x%x%s" % (path, base, "" if symbols == path else " (" + symbols + ")")) + + +OkSymbols() diff --git a/tools/scripts/okgdb_common.py b/tools/scripts/okgdb_common.py new file mode 100644 index 0000000..deedbcc --- /dev/null +++ b/tools/scripts/okgdb_common.py @@ -0,0 +1,88 @@ +# What okgdb.py and oklldb.py share: an ok image's segments, its build-id, its +# debug file, and its base from a mapped part. +import os +import struct + + +def segments(path): + """The loadable segments of an ELF file: (offset, vaddr, filesz).""" + with open(path, "rb") as f: + head = f.read(64) + if head[:4] != b"\x7fELF" or head[4] != 2: + return [] + phoff, = struct.unpack_from("= seg_offset else start - (vaddr - (seg_offset - offset)) + return None diff --git a/tools/scripts/oklldb.py b/tools/scripts/oklldb.py new file mode 100644 index 0000000..33edbb7 --- /dev/null +++ b/tools/scripts/oklldb.py @@ -0,0 +1,50 @@ +# oklldb.py --- the symbols of the ok images a program has mapped, for lldb. +# +# (lldb) command script import oklldb.py +# (lldb) ok-symbols +# +# As okgdb.py: each mapped part of an image names the image's file and, with +# the part's place in the image, its base; each image is added as a module and +# loaded with that slide. A debug file `okld debug-split' wrote is found by the +# build-id beside the image or in OKGDB_DEBUG_DIR. lldb reads an image the +# runner copied through the GDB JIT interface +# (`settings set plugin.jit-loader.gdb.enable on'). +import os +import sys + +import lldb + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import okgdb_common as common # noqa: E402 + + +def ok_symbols(debugger, command, result, internal_dict): + target = debugger.GetSelectedTarget() + process = target.GetProcess() + regions = process.GetMemoryRegions() + bases = {} + info = lldb.SBMemoryRegionInfo() + for i in range(regions.GetSize()): + if not regions.GetMemoryRegionAtIndex(i, info): + continue + path = info.GetName() + if not path or path in bases or not os.path.exists(path) or not common.is_image(path): + continue + # lldb does not report a region's offset in its file: the image's first + # mapped part is matched against its segments by the bytes it holds. + start = info.GetRegionBase() + base = common.base_of(path, start, lambda at, n: process.ReadMemory(at, n, lldb.SBError())) + if base is not None: + bases[path] = base + if not bases: + result.AppendMessage("ok-symbols: no ok image is mapped (one the runner copied is announced through the JIT interface)") + return + for path, base in sorted(bases.items()): + symbols = common.debug_file(path) + debugger.HandleCommand("target modules add %s" % symbols) + debugger.HandleCommand("target modules load --file %s --slide 0x%x" % (os.path.basename(symbols), base)) + result.AppendMessage("ok-symbols: %s at 0x%x" % (path, base)) + + +def __lldb_init_module(debugger, internal_dict): + debugger.HandleCommand("command script add -f oklldb.ok_symbols ok-symbols") diff --git a/tools/src/okld.cpp b/tools/src/okld.cpp index b7c1a47..9e62640 100644 --- a/tools/src/okld.cpp +++ b/tools/src/okld.cpp @@ -9,10 +9,22 @@ // appended, and the placement note filled (SPEC 12.4, // type 3): a loader that places the image there // applies no relative relocation +// okld touch ... +// the pages the image's start executed first, as runs +// of its file's 64 KiB blocks in that order, written +// into its touch note (SPEC 12.4, type 4) from the +// reports `okrun --prof=touch' writes; a loader reads +// them in with their part. Before `okld sign'. +// okld debug-split [-o ] +// the image without what only a debugger reads (its +// section table and the sections it alone names), and +// the whole of it as /.build-id/xx/rest.debug, +// named by the image's build-id as debuginfod names one +// (okgdb.py and oklldb.py look there). Before `okld note'. // okld sign the signature note filled (Ed25519); prints the key // okld keygen a new seed, written; prints its public key // okld inspect what the image is -// okld note-source the assembly that lays down the three notes, for the link +// okld note-source the assembly that lays down the four notes, for the link // okld bundle [--name ] [--version ] [--entry ] [--sign ] ... // an okb (docs/okb.md): the images, by architecture, and // okb.json; with --sign, each image and okb.json signed @@ -75,6 +87,8 @@ int check(const std::vector& image) { [](const char* n, void*) { return surface().contains(n) ? 1 : 0; }, [](const char* n, void*) { return g_elsewhere.contains(n) ? 1 : 0; }, [](const char* s, void*) { std::println(std::cerr, "okld: {}", s); }, nullptr); + failures += okld_check_placement(image.data(), image.size(), + [](const char* s, void*) { std::println(std::cerr, "okld: {}", s); }, nullptr); return failures == 0 ? 0 : 1; } @@ -502,6 +516,12 @@ int bundle_check(const std::vector& args) { std::println(std::cerr, "okld: {}: {}: {}", *c.path, *c.name, sentence); ++*c.failures; }, &c); + okld_check_placement(image, m->size, + [](const char* sentence, void* p) { + auto& c = *static_cast(p); + std::println(std::cerr, "okld: {}: {}: {}", *c.path, *c.name, sentence); + ++*c.failures; + }, &c); } } if (failures == 0) { @@ -517,24 +537,168 @@ int bundle_check(const std::vector& args) { } // namespace +// --- the touch note, from the runner's reports (0.4) --------------------------------- +// +// A report's touches: `"touches": [{ "image": , "offset": }, ...]', +// read by a scan for the two keys, in order. +std::vector> touches_of(const std::string& text) { + std::vector> out; + const auto start = text.find("\"touches\""); + if (start == std::string::npos) return out; + std::size_t at = start; + std::string image; + for (;;) { + const auto i = text.find("\"image\"", at), o = text.find("\"offset\"", at); + if (o == std::string::npos) break; + if (i != std::string::npos && i < o) { + const auto q = text.find('"', text.find(':', i) + 1); + const auto e = text.find('"', q + 1); + image = text.substr(q + 1, e - q - 1); + } + std::size_t k = text.find(':', o) + 1; + while (k < text.size() && text[k] == ' ') ++k; + std::uint64_t v = 0; + while (k < text.size() && text[k] >= '0' && text[k] <= '9') v = v * 10 + static_cast(text[k++] - '0'); + out.emplace_back(image, v); + at = k; + const auto close = text.find(']', start); + if (close != std::string::npos && at > close) break; + } + return out; +} + +int touch(const std::vector& args) { + if (args.size() < 3) { std::println(std::cerr, "okld touch ..."); return 2; } + auto image = read_file(args[1]); + if (!image) { std::println(std::cerr, "okld: {} cannot be read", args[1]); return 1; } + std::vector files; + for (std::size_t i = 2; i < args.size(); ++i) { + std::error_code ec; + if (std::filesystem::is_directory(args[i], ec)) { + for (auto const& e : std::filesystem::directory_iterator(args[i], ec)) + if (e.path().filename().string().starts_with("okprof-") && e.path().extension() == ".json") files.push_back(e.path()); + } else files.push_back(args[i]); + } + std::ranges::sort(files); + // The blocks in the order first touched, each once, over every report: a + // page another start touched first is placed where that start first did. + const auto stem = std::filesystem::path(args[1]).filename().string(); + std::vector order; + std::set seen; + for (auto const& f : files) { + auto bytes = read_file(f.string()); + if (!bytes) continue; + const std::string text(bytes->begin(), bytes->end()); + auto t = touches_of(text); + bool named = false; + for (auto const& [img, off] : t) named = named || std::filesystem::path(img).filename().string() == stem; + for (auto const& [img, off] : t) { + if (named && std::filesystem::path(img).filename().string() != stem) continue; + const okld_u32 block = static_cast(off / 65536); + if (seen.insert(block).second) order.push_back(block); + } + } + if (order.empty()) { std::println(std::cerr, "okld: the reports record no touch of {}", stem); return 1; } + // Runs of blocks that follow one another in the order. + std::vector ranges; + for (std::size_t i = 0; i < order.size();) { + std::size_t j = i + 1; + while (j < order.size() && order[j] == order[j - 1] + 1) ++j; + ranges.push_back(order[i]); + ranges.push_back(static_cast(j - i)); + i = j; + } + const okld_u32 count = static_cast(std::min(ranges.size() / 2, okld::touch_max)); + okld_why why{}; + if (okld_touch(image->data(), image->size(), ranges.data(), count, &why) != 0) { + std::println(std::cerr, "okld: {}: {}", args[1], why.text); + return 1; + } + if (!write_file(args[1], *image)) return 1; + std::println("{}: {} block(s) in {} range(s){}", args[1], order.size(), count, + count < ranges.size() / 2 ? " (the first of them; the note holds no more)" : ""); + return 0; +} + +// --- debug-split (0.4) ------------------------------------------------------------- +// +// THE IMAGE A PROGRAM RUNS NEEDS ITS SEGMENTS, AND A DEBUGGER NEEDS EVERYTHING. +// The whole file is kept, as the debug file a debugger reads by the image's +// build-id; the image keeps what its segments hold and drops the section +// table and what lies only beyond the segments --- the debug sections and the +// symbol table, which a loader and the runner do not read (the runner names +// addresses from the debug file where the image has no table of its own). +int debug_split(const std::vector& args) { + if (args.size() != 2 && !(args.size() == 4 && args[2] == "-o")) { + std::println(std::cerr, "okld debug-split [-o ]"); + return 2; + } + auto image = read_file(args[1]); + if (!image || image->size() < 64) { std::println(std::cerr, "okld: {} cannot be read", args[1]); return 1; } + auto& b = *image; + std::uint64_t phoff = 0, shoff = 0; std::uint16_t phnum = 0; + std::memcpy(&phoff, b.data() + 32, 8); std::memcpy(&shoff, b.data() + 40, 8); std::memcpy(&phnum, b.data() + 56, 2); + if (shoff == 0) { std::println(std::cerr, "okld: {} has no section table: it is split already", args[1]); return 1; } + // The build-id: the GNU note of type 3 in a note segment. + std::string id; + std::uint64_t end = phoff + static_cast(phnum) * 56; + for (unsigned i = 0; i < phnum; ++i) { + const auto* ph = b.data() + phoff + i * 56; + std::uint32_t type = 0; std::uint64_t off = 0, fs = 0; + std::memcpy(&type, ph, 4); std::memcpy(&off, ph + 8, 8); std::memcpy(&fs, ph + 32, 8); + if (off + fs > end) end = off + fs; + if (type != 4 /* PT_NOTE */) continue; + for (std::uint64_t at = off; at + 12 <= off + fs;) { + std::uint32_t namesz = 0, descsz = 0, ntype = 0; + std::memcpy(&namesz, b.data() + at, 4); std::memcpy(&descsz, b.data() + at + 4, 4); std::memcpy(&ntype, b.data() + at + 8, 4); + const std::uint64_t desc = at + 12 + ((namesz + 3u) & ~3u); + if (namesz == 4 && std::memcmp(b.data() + at + 12, "GNU", 4) == 0 && ntype == 3) + id = hex(b.data() + desc, descsz); + at = desc + ((descsz + 3u) & ~3u); + } + } + if (id.size() < 4) { + std::println(std::cerr, "okld: {} has no build-id (link with --build-id)", args[1]); + return 1; + } + const std::filesystem::path dir = args.size() == 4 ? std::filesystem::path(args[3]) : std::filesystem::path(args[1]).parent_path(); + const auto debug = dir / ".build-id" / id.substr(0, 2) / (id.substr(2) + ".debug"); + std::error_code ec; + std::filesystem::create_directories(debug.parent_path(), ec); + if (!write_file(debug.string(), b)) return 1; + // The image: its segments and what precedes them, no section table. + std::vector kept(b.begin(), b.begin() + static_cast(std::min(end, b.size()))); + const std::uint64_t zero8 = 0; const std::uint16_t zero2 = 0; + std::memcpy(kept.data() + 40, &zero8, 8); // e_shoff + std::memcpy(kept.data() + 60, &zero2, 2); // e_shnum + std::memcpy(kept.data() + 62, &zero2, 2); // e_shstrndx + if (!write_file(args[1], kept)) return 1; + std::println("{}: {} bytes kept; {} bytes for the debugger in {}", args[1], kept.size(), b.size(), debug.string()); + return 0; +} + int main(int argc, char** argv) { const std::vector args(argv + 1, argv + argc); - auto usage = [] { std::println(std::cerr, "usage: okld check|note|place|inspect | sign | keygen | note-source | bundle ... | bundle check "); return 2; }; + auto usage = [] { std::println(std::cerr, "usage: okld check|note|place|inspect | touch ... | debug-split | sign | keygen | note-source | bundle ... | bundle check "); return 2; }; if (args.empty()) return usage(); const auto& verb = args[0]; if (verb == "note-source") { std::print("{}", - "# The three notes of an ok image (SPEC 12.4): requirements, written by `okld note';\n" - "# a signature, written by `okld sign'; and a placement, written by `okld place'\n" - "# (format 1.1). The last two are all zero until then.\n" + "# The four notes of an ok image (SPEC 12.4): requirements, written by `okld note';\n" + "# a signature, written by `okld sign'; a placement, written by `okld place'\n" + "# (format 1.1); and the pages it touches when it starts, written by\n" + "# `okld touch' (format 1.1, room for 512 ranges). The last three are all zero\n" + "# until then.\n" "\t.section .note.openkal,\"a\",@note\n" "\t.p2align 2\n" "\t.long 8, 16, 1\n\t.asciz \"OPENKAL\"\n\t.long 1, 1\n\t.quad 0\n" "\t.p2align 2\n" "\t.long 8, 96, 2\n\t.asciz \"OPENKAL\"\n\t.fill 96, 1, 0\n" "\t.p2align 2\n" - "\t.long 8, 208, 3\n\t.asciz \"OPENKAL\"\n\t.fill 208, 1, 0\n"); + "\t.long 8, 208, 3\n\t.asciz \"OPENKAL\"\n\t.fill 208, 1, 0\n" + "\t.p2align 2\n" + "\t.long 8, 4104, 4\n\t.asciz \"OPENKAL\"\n\t.fill 4104, 1, 0\n"); return 0; } if (verb == "keygen" && args.size() == 2) { @@ -548,6 +712,8 @@ int main(int argc, char** argv) { return 0; } if (verb == "bundle" && args.size() >= 2 && args[1] == "check") return bundle_check(args); + if (verb == "touch") return touch(args); + if (verb == "debug-split") return debug_split(args); if (verb == "bundle") return bundle(args); if (args.size() < 2) return usage(); auto image = read_file(args[1]); @@ -616,6 +782,14 @@ int main(int argc, char** argv) { std::println("placement {:#x}, {} writable segment(s) stored", base, count); else std::println("placement none"); + { + std::vector ranges(2 * okld::touch_max); + const okld_u32 n = okld_touches(image->data(), image->size(), ranges.data(), okld::touch_max); + okld_u64 blocks = 0; + for (okld_u32 i = 0; i < n && i < okld::touch_max; ++i) blocks += ranges[2 * i + 1]; + if (n) std::println("touches {} block(s) in {} range(s)", blocks, n); + else std::println("touches none"); + } std::println("entry {:#x}", info.entry); std::println("needs {} image(s)", info.needed_count); std::vector imports; diff --git a/tools/src/okprof.cpp b/tools/src/okprof.cpp index a6100ec..a1cb6c8 100644 --- a/tools/src/okprof.cpp +++ b/tools/src/okprof.cpp @@ -10,14 +10,27 @@ // the calls each image made across openkal, by name and by interface, // with their counts and time; the counters and phases the program stated. // Several reports are summed. +// okprof report --flame ... +// the stacks sampled (`samples'), folded --- the outermost frame first, +// `;' between, then the count --- which a flame graph is drawn from. +// okprof report --format=trace ... +// a timeline for Perfetto or chrome://tracing: a row for each program, +// its start's moments (`phases') and each sample on one time axis. +// okprof report --order ... +// the image's functions in the order its start first executed their pages +// (`touch'): a symbol ordering file, which a link lays the code out by. +// okprof report --names +// the names a program reached through its directories (`record --what +// names'), by directory: how many it asked for and found absent. // okprof diff // the same names side by side, ordered by how much their time moved: the // input of a gate that compares two runs (an image and the same program // built for its system; two versions). // // A report is the runner's (run/src/prof.cpp); this program only reads and -// starts. It does not sample: a profiler of the system does, and on Linux the -// runner's `perfmap' names an image's addresses for it. +// starts. Samples and touches are taken by the implementation (openkal 0.23) +// and named by the runner; on Linux the runner's `perfmap' also names an +// image's addresses for the system's profiler. #include #include import std; @@ -25,8 +38,22 @@ import std; namespace { struct row { std::string name; std::uint64_t count = 0, ns = 0, value = 0; }; +struct moment { std::string name; std::uint64_t ns = 0, faults = 0; }; +struct sample_at { std::uint64_t ns = 0, stack = 0, context = 0; }; +// One program's report, for what is not summed: a timeline, an order. +struct program { + std::uint64_t id = 0, started_ns = 0; + std::string image; + std::vector moments; + std::vector stacks; + std::vector timeline; + std::vector> touches; // image, file offset + std::uint64_t page = 4096; +}; struct report { std::map calls, counters, phases; + std::map folded; // a stack, and how often it was sampled + std::vector programs; std::uint64_t wall_ns = 0; int files = 0; }; @@ -99,12 +126,38 @@ bool read_rows(reader& r, std::map& into) { return r.eat(']'); } +// An array of flat objects, each given to `each' as its keys' strings and numbers. +template +bool read_objects(reader& r, Each each) { + if (!r.eat('[')) return false; + if (r.eat(']')) return true; + do { + if (!r.eat('{')) return false; + std::map text; + std::map number; + if (!r.eat('}')) { + do { + auto key = r.text(); + if (!key || !r.eat(':')) return false; + r.space(); + if (r.at < r.s.size() && r.s[r.at] == '"') { auto v = r.text(); if (!v) return false; text[*key] = *v; } + else if (auto v = r.number()) number[*key] = *v; + else r.skip_value(); + } while (r.eat(',')); + if (!r.eat('}')) return false; + } + each(text, number); + } while (r.eat(',')); + return r.eat(']'); +} + bool read_report(const std::filesystem::path& file, report& into) { std::ifstream in(file, std::ios::binary); if (!in) return false; const std::string text((std::istreambuf_iterator(in)), {}); reader r{ text }; if (!r.eat('{')) return false; + program one; do { auto key = r.text(); if (!key || !r.eat(':')) return false; @@ -112,8 +165,37 @@ bool read_report(const std::filesystem::path& file, report& into) { else if (*key == "counters") { if (!read_rows(r, into.counters)) return false; } else if (*key == "phases") { if (!read_rows(r, into.phases)) return false; } else if (*key == "wall_ns") { auto v = r.number(); if (v) into.wall_ns += *v; } + else if (*key == "id") { auto v = r.number(); if (v) one.id = *v; } + else if (*key == "started_ns") { auto v = r.number(); if (v) one.started_ns = *v; } + else if (*key == "image") { auto v = r.text(); if (v) one.image = *v; } + else if (*key == "page") { auto v = r.number(); if (v) one.page = *v; } + else if (*key == "moments") { + if (!read_objects(r, [&](auto& t, auto& n) { one.moments.push_back({ t["name"], n["ns"], n["faults"] }); })) return false; + } else if (*key == "stacks") { + if (!read_objects(r, [&](auto& t, auto& n) { + one.stacks.push_back(t["stack"]); + into.folded[t["stack"]] += n["count"]; + })) return false; + } else if (*key == "touches") { + if (!read_objects(r, [&](auto& t, auto& n) { one.touches.emplace_back(t["image"], n["offset"]); })) return false; + } else if (*key == "timeline") { + // [[ns, stack, context], ...] + if (!r.eat('[')) return false; + if (!r.eat(']')) { + do { + if (!r.eat('[')) return false; + sample_at at; + auto a = r.number(); r.eat(','); auto b = r.number(); r.eat(','); auto c = r.number(); + if (!a || !b || !r.eat(']')) return false; + at.ns = *a; at.stack = *b; at.context = c ? *c : 0; + one.timeline.push_back(at); + } while (r.eat(',')); + if (!r.eat(']')) return false; + } + } else r.skip_value(); } while (r.eat(',')); + into.programs.push_back(std::move(one)); ++into.files; return true; } @@ -153,9 +235,166 @@ std::string interface_of(const std::string& name) { std::string ms(std::uint64_t ns) { return std::format("{:.3f} ms", static_cast(ns) / 1e6); } -int report_cmd(const std::vector& args) { +// --- the forms of a report beside the summary ----------------------------------------- + +// Folded stacks: `outer;...;inner count', one a line. +int flame(const report& r) { + for (auto const& [stack, count] : r.folded) std::println("{} {}", stack, count); + return r.folded.empty() ? 1 : 0; +} + +std::string json_text(const std::string& s) { + std::string o = "\""; + for (char c : s) { + if (c == '"' || c == '\\') { o += '\\'; o += c; } + else if (static_cast(c) < 0x20) o += '?'; + else o += c; + } + return o + "\""; +} + +// The Trace Event Format, which Perfetto and chrome://tracing read: each program a +// process, its start's moments as spans one after another, each sample an +// instant on its context's row, all on the monotonic clock the reports share. +int trace(const report& r) { + std::uint64_t first = ~0ull; + for (auto const& p : r.programs) if (p.started_ns && p.started_ns < first) first = p.started_ns; + if (first == ~0ull) first = 0; + std::println("{{\"traceEvents\": ["); + bool any = false; + auto event = [&](const std::string& e) { std::print("{} {}", any ? ",\n" : "", e); any = true; }; + for (auto const& p : r.programs) { + const double base = static_cast(p.started_ns - first) / 1000.0; + event(std::format("{{\"name\": \"process_name\", \"ph\": \"M\", \"pid\": {}, \"args\": {{\"name\": {}}}}}", + p.id, json_text(std::filesystem::path(p.image).filename().string() + " " + std::to_string(p.id)))); + std::uint64_t from = 0; + for (auto const& m : p.moments) { + event(std::format("{{\"name\": {}, \"ph\": \"X\", \"pid\": {}, \"tid\": 0, \"ts\": {:.3f}, \"dur\": {:.3f}, \"args\": {{\"faults\": {}}}}}", + json_text("to " + m.name), p.id, base + static_cast(from) / 1000.0, + static_cast(m.ns - from) / 1000.0, m.faults)); + from = m.ns; + } + for (auto const& s : p.timeline) { + const std::string& stack = s.stack < p.stacks.size() ? p.stacks[s.stack] : std::string("?"); + const auto leaf = stack.substr(stack.rfind(';') == std::string::npos ? 0 : stack.rfind(';') + 1); + event(std::format("{{\"name\": {}, \"ph\": \"i\", \"s\": \"t\", \"pid\": {}, \"tid\": {}, \"ts\": {:.3f}, \"args\": {{\"stack\": {}}}}}", + json_text(leaf), p.id, s.context, base + static_cast(s.ns) / 1000.0, json_text(stack))); + } + } + std::println("\n]}}"); + return 0; +} + +std::optional> read_bytes(const std::string& path) { + std::ifstream in(path, std::ios::binary); + if (!in) return std::nullopt; + return std::vector((std::istreambuf_iterator(in)), {}); +} + +// A symbol ordering file: the image's functions whose pages its start executed, +// in the order it first executed them, each once; within a page, in address +// order. +int order(const std::string& image_path, const report& r) { + auto image = read_bytes(image_path); + if (!image) { std::println(std::cerr, "okprof: {} cannot be read", image_path); return 2; } + struct sym { std::uint64_t value, size; std::string name; }; + std::vector syms; + okld_symbols(image->data(), image->size(), [](okld_u64 v, okld_u64 n, const char* name, void* c) { + static_cast*>(c)->push_back({ v, n, name }); + }, &syms); + std::ranges::sort(syms, [](const sym& a, const sym& b) { return a.value < b.value; }); + // The file offset of a page to the address of the image: its segment. + struct seg { std::uint64_t offset, vaddr, filesz; }; + std::vector segs; + { + const auto* b = image->data(); + std::uint64_t phoff = 0; std::uint16_t phnum = 0; + std::memcpy(&phoff, b + 32, 8); std::memcpy(&phnum, b + 56, 2); + for (unsigned i = 0; i < phnum; ++i) { + std::uint32_t type = 0; std::uint64_t off = 0, va = 0, fs = 0; + const auto* ph = b + phoff + i * 56; + std::memcpy(&type, ph, 4); std::memcpy(&off, ph + 8, 8); std::memcpy(&va, ph + 16, 8); std::memcpy(&fs, ph + 32, 8); + if (type == 1) segs.push_back({ off, va, fs }); + } + } + // The touches of the image by its file's name; where no report names it so + // (the image measured was installed under another), those of the image each + // report ran. + const auto stem = std::filesystem::path(image_path).filename().string(); + auto named = [&](const std::string& path) { return std::filesystem::path(path).filename().string() == stem; }; + bool by_name = false; + for (auto const& p : r.programs) + for (auto const& t : p.touches) by_name = by_name || named(t.first); + std::set given; + std::uint64_t pages = 0; + for (auto const& p : r.programs) + for (auto const& [img, offset] : p.touches) { + if (by_name ? !named(img) : img != p.image) continue; + ++pages; + for (auto const& s : segs) { + if (offset < s.offset || offset >= s.offset + s.filesz) continue; + const std::uint64_t lo = s.vaddr + (offset - s.offset), hi = lo + p.page; + auto it = std::ranges::lower_bound(syms, lo, {}, [](const sym& x) { return x.value + (x.size ? x.size : 1); }); + for (; it != syms.end() && it->value < hi; ++it) + if (given.insert(it->name).second) std::println("{}", it->name); + } + } + if (pages == 0) { std::println(std::cerr, "okprof: the reports record no touch of {}", stem); return 1; } + return 0; +} + +// The names reached through each directory a program was granted (`record +// --what names'): the records, `\0' each, by directory. +int names(const std::string& dir) { + std::map> by; // r w s a l + std::uint64_t ended = 0, records = 0; + std::error_code ec; + for (auto const& e : std::filesystem::directory_iterator(dir, ec)) { + if (e.path().extension() != ".record") continue; + ++records; + auto bytes = read_bytes(e.path().string()); + if (!bytes) continue; + std::string root; + if (std::ifstream named(e.path().string() + ".root"); named) std::getline(named, root); + std::size_t at = 0; + while (at < bytes->size()) { + const std::size_t end = std::find(bytes->begin() + static_cast(at), bytes->end(), '\0') - bytes->begin(); + if (end >= bytes->size() || end == at) break; + const char kind = static_cast((*bytes)[at]); + const std::string name(reinterpret_cast(bytes->data()) + at + 1, end - at - 1); + at = end + 1; + if (kind == 'e') { ++ended; continue; } + const auto slash = name.rfind('/'); + const std::string where = root + (root.ends_with("/") || root.empty() ? "" : "/") + (slash == std::string::npos ? "" : name.substr(0, slash)); + const int k = kind == 'r' ? 0 : kind == 'w' ? 1 : kind == 's' ? 2 : kind == 'a' ? 3 : kind == 'l' ? 4 : -1; + if (k >= 0) ++by[where][static_cast(k)]; + } + } + if (records == 0) { std::println(std::cerr, "okprof: {} holds no record", dir); return 2; } + std::vector>> rows(by.begin(), by.end()); + std::ranges::sort(rows, [](auto const& a, auto const& b) { return a.second[3] > b.second[3]; }); + std::println("{} record{}, {} program end{} written", records, records == 1 ? "" : "s", ended, ended == 1 ? "" : "s"); + std::println("{:>9} {:>9} {:>9} {:>9} {:>9} directory", "absent", "present", "read", "written", "listed"); + for (auto const& [where, n] : rows) + std::println("{:>9} {:>9} {:>9} {:>9} {:>9} {}", n[3], n[2], n[0], n[1], n[4], where.empty() ? "." : where); + return 0; +} + +int report_cmd(std::vector args) { + std::string form, image; + while (!args.empty() && args.front().starts_with("--")) { + if (args.front() == "--flame") form = "flame"; + else if (args.front() == "--format=trace") form = "trace"; + else if (args.front() == "--order" && args.size() > 1) { form = "order"; image = args[1]; args.erase(args.begin()); } + else if (args.front() == "--names" && args.size() > 1) return names(args[1]); + else { std::println(std::cerr, "okprof report: unknown option {}", args.front()); return 2; } + args.erase(args.begin()); + } report r; if (!read_reports(args, r)) return 2; + if (form == "flame") return flame(r); + if (form == "trace") return trace(r); + if (form == "order") return order(image, r); std::println("{} report{}; the images ran {} in all", r.files, r.files == 1 ? "" : "s", ms(r.wall_ns)); std::vector calls; for (auto const& [k, v] : r.calls) calls.push_back(v); @@ -187,6 +426,33 @@ int report_cmd(const std::vector& args) { std::println("\nphases the program stated"); for (auto const& [k, v] : r.phases) std::println(" {:<40} {:>8} times {:>14}", k, v.count, ms(v.ns)); } + for (auto const& p : r.programs) { + if (p.moments.empty()) continue; + std::println("\nthe start of {} ({}), to each moment", std::filesystem::path(p.image).filename().string(), p.id); + std::uint64_t ns = 0, faults = 0; + for (auto const& m : p.moments) { + std::println(" {:<14} {:>12} {:>8} faults (+{}, +{})", m.name, ms(m.ns), m.faults, ms(m.ns - ns), m.faults - faults); + ns = m.ns; faults = m.faults; + } + } + if (!r.folded.empty()) { + std::map leaves; + std::uint64_t all = 0; + for (auto const& [stack, count] : r.folded) { + const auto cut = stack.rfind(';'); + leaves[cut == std::string::npos ? stack : stack.substr(cut + 1)] += count; + all += count; + } + std::vector> top(leaves.begin(), leaves.end()); + std::ranges::sort(top, [](auto const& a, auto const& b) { return a.second > b.second; }); + std::println("\nwhere the samples fell ({} in all; `--flame' gives the stacks)", all); + for (std::size_t i = 0; i < top.size() && i < 20; ++i) + std::println(" {:>6.2f}% {}", 100.0 * static_cast(top[i].second) / static_cast(all), top[i].first); + } + for (auto const& p : r.programs) + if (!p.touches.empty()) + std::println("\n{} pages of {} executed first by its start (`--order ', `okld touch')", + p.touches.size(), std::filesystem::path(p.image).filename().string()); return 0; } @@ -270,6 +536,59 @@ std::optional on_path(const std::string& name) { return std::nullopt; } +// The directories this program was given, each granted to the program started, +// under its own name and recorded (`names', openkal 0.23): a record of each, +// `/.record', with the name it was granted under beside it. +struct recorded_grants { + std::vector grants; + std::vector names; + std::vector records; + std::vector dirs; // the records' directory, and each granted one + std::vector granted; + ~recorded_grants() { + for (auto d : granted) kal_fs_close_dir(d); + for (auto d : dirs) kal_fs_close_dir(d); + for (auto f : records) kal_fs_close_file(f); + } +}; + +bool grant_recorded(const std::string& dir, recorded_grants& out) { + kal_dir into{}; std::string rest; + std::error_code ec; + const auto absolute = std::filesystem::absolute(dir, ec).generic_string(); + if (!locate(absolute, into, rest)) return false; + kal_dir d{}; + if (kal_fs_open_dir(into, rest.c_str(), rest.size(), &d) != kal_ok) return false; + out.dirs.push_back(d); + const kal_uintptr n = kal_fs_preopen_count(); + out.names.reserve(n); + for (kal_uintptr i = 0; i < n; ++i) { + kal_dir pre{}; char name[1024]; kal_uintptr len = 0; + if (kal_fs_preopen(i, &pre, name, sizeof name, &len) != kal_ok || len >= sizeof name) continue; + if ((kal_fs_props(pre) & KAL_FS_PROP_ATTENUATE) == 0) { + std::println(std::cerr, "okprof: this system's directories are not attenuated; names are not recorded"); + return false; + } + const std::string file = std::format("{}.record", i); + kal_file rec{}; + if (kal_fs_open(d, file.c_str(), file.size(), KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_TRUNCATE, &rec) != kal_ok) + return false; + out.records.push_back(rec); + if (std::ofstream root(absolute + "/" + file + ".root"); root) root << std::string_view(name, len) << "\n"; + kal_dir_rights rights{}; + rights.self_size = sizeof rights; + rights.flags = KAL_DIR_RECORDED; + rights.record = rec; + kal_dir granted{}; + if (kal_fs_dir_attenuate(pre, &rights, &granted) != kal_ok) return false; + out.granted.push_back(granted); + out.names.emplace_back(name, len); + } + for (std::size_t i = 0; i < out.names.size(); ++i) + out.grants.push_back(kal_preopen{ out.granted[i], out.names[i].c_str(), out.names[i].size() }); + return !out.grants.empty(); +} + int record_cmd(std::vector args) { std::string dir = ".", what = "calls,counters"; while (!args.empty() && args.front() != "--") { @@ -287,8 +606,22 @@ int record_cmd(std::vector args) { kal_dir base{}; std::string rest; if (!locate(*program, base, rest)) { std::println(std::cerr, "okprof: {} is beneath no directory this program was given", *program); return 2; } + // `names' is this program's to take, by the directories it grants; the rest + // is the runner's. + bool record_names = false; + std::string runner_what; + for (auto part : std::views::split(std::string_view(what), ',')) { + const std::string one(part.begin(), part.end()); + if (one == "names") { record_names = true; continue; } + if (!one.empty()) runner_what += (runner_what.empty() ? "" : ",") + one; + } + recorded_grants grants; + if (record_names && !grant_recorded(dir, grants)) { + std::println(std::cerr, "okprof: the directories could not be granted recorded"); + return 2; + } // The environment, with OKRUN_PROF stated (replacing any it had). - const std::string spec = std::format("OKRUN_PROF={}:{}", what, std::filesystem::absolute(dir, ec).generic_string()); + const std::string spec = std::format("OKRUN_PROF={}:{}", runner_what, std::filesystem::absolute(dir, ec).generic_string()); std::vector env; for (kal_uintptr i = 0; i < kal_env_var_count(); ++i) { char entry[16384]; @@ -298,7 +631,7 @@ int record_cmd(std::vector args) { if (e.starts_with("OKRUN_PROF=")) continue; env.push_back(std::move(e)); } - env.push_back(spec); + if (!runner_what.empty()) env.push_back(spec); std::vector argv, envp; std::vector argl, envl; for (auto const& a : args) { argv.push_back(a.c_str()); argl.push_back(a.size()); } @@ -308,7 +641,8 @@ int record_cmd(std::vector args) { char name[8]; kal_uintptr len = 0; kal_fs_preopen(0, &work, name, sizeof name, &len); } - const kal_spawn how{ base, work, nullptr, nullptr, 0, 0 }; + const kal_spawn how{ base, work, nullptr, record_names ? grants.grants.data() : nullptr, + record_names ? grants.grants.size() : 0, 0 }; const kal_spawn_streams streams{ 0, 0, 0 }; kal_process child{}; const int e = kal_process_spawn(&how, rest.c_str(), rest.size(), argv.data(), argl.data(), argv.size(), @@ -317,7 +651,8 @@ int record_cmd(std::vector args) { int status = 0, terminated = 0; kal_process_wait(child, &status, &terminated); kal_process_close(child); - std::println(std::cerr, "okprof: the reports are in {}; `okprof report {}' reads them", dir, dir); + std::println(std::cerr, "okprof: the reports are in {}; `okprof report {}{}' reads them", dir, + record_names ? "--names " : "", dir); return terminated ? 128 + status : status; } @@ -332,7 +667,7 @@ int main(int argc, char** argv) { const std::string command = args.front(); args.erase(args.begin()); if (command == "record") return record_cmd(std::move(args)); - if (command == "report") return report_cmd(args); + if (command == "report") return report_cmd(std::move(args)); if (command == "diff") return diff_cmd(args); std::println(std::cerr, "okprof: unknown command {}", command); return 2; From 9e6a62017fe59cf73ea980193c20afdbd053b98a Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 07:35:30 +0900 Subject: [PATCH 3/9] wip: a snapshot of an image's initialisers (docs/snapshot.md) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 63 ++++- docs/snapshot.md | 127 +++++++++ examples/snapshot/app.c | 60 +++++ examples/snapshot/timed.c | 4 + ld/include/okld.h | 37 ++- ld/src/load.cpp | 63 ++++- ld/src/okld.cppm | 2 + ld/src/place.cpp | 29 ++- run/mcpp.toml | 2 +- run/src/bindings.cpp | 6 +- run/src/bindings_space.cpp | 3 + run/src/no_space.cpp | 2 + run/src/run.cpp | 518 ++++++++++++++++++++++++++++++++++--- run/src/runner.h | 32 +++ run/src/seal.cpp | 371 ++++++++++++++++++++++++++ 15 files changed, 1244 insertions(+), 75 deletions(-) create mode 100644 docs/snapshot.md create mode 100644 examples/snapshot/app.c create mode 100644 examples/snapshot/timed.c create mode 100644 run/src/seal.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48ac59b..950340a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,10 @@ jobs: # this one is also left unplaced, its placement note empty. bash examples/build-image.sh "$arch" "out/$arch/placed.oke" examples/placed/app.c PLACE=0 bash examples/build-image.sh "$arch" "out/$arch/unplaced.oke" examples/placed/app.c + # A SNAPSHOT OF THE INITIALISERS (0.4.0): an image whose C library + # takes part, and one whose initialiser asks for the time. + bash examples/build-image.sh "$arch" "out/$arch/snapshot.oke" examples/snapshot/app.c + bash examples/build-image.sh "$arch" "out/$arch/snapshot-timed.oke" examples/snapshot/timed.c "$OKLD" inspect "out/$arch/placed.oke" | awk '$1 == "placement" { sub(",", "", $2); print $2 }' > "out/$arch/placed.base" grep -q '^0x' "out/$arch/placed.base" || { echo "::error::okld place stated no base"; exit 1; } "$OKLD" inspect "out/$arch/unplaced.oke" | grep -q '^placement *none' \ @@ -241,23 +245,68 @@ jobs: - name: At its base (0.4.0) run: | # Placed where the implementation places memory where it is asked - # (KAL_EXEC_PROP_PLACE): at the base `okld place' chose, its relative - # relocations already in its bytes. Anywhere (OKRUN_PLACE=0), and an - # image left unplaced: relocated. What the image prints is the same. + # (KAL_EXEC_PROP_PLACE): at this boot's base for the image, its data + # prepared once for that base in the runner's cache; at the base + # `okld place' chose (OKRUN_PLACE=sealed), its relative relocations + # already in its bytes. Anywhere (OKRUN_PLACE=0), and an image left + # unplaced: relocated. What the image prints is the same. a='${{ matrix.arch }}' base=$(cat "out/$a/placed.base") - "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > placed.txt + export OKRUN_CACHE="$RUNNER_TEMP/okplaced" + mkdir -p "$OKRUN_CACHE" + "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > local.txt + "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > again.txt + OKRUN_PLACE=sealed "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > placed.txt OKRUN_PLACE=0 "$OKRUN" "out/$a/placed.oke" | tr -d '\r' > anywhere.txt "$OKRUN" "out/$a/unplaced.oke" | tr -d '\r' > unplaced.txt - cat placed.txt anywhere.txt unplaced.txt + cat local.txt placed.txt anywhere.txt unplaced.txt + ls "$OKRUN_CACHE" grep -qx "base $base" placed.txt || { echo "::error::the image was not placed at the base it states ($base)"; exit 1; } if grep -qx "base $base" anywhere.txt; then echo "::error::OKRUN_PLACE=0 placed the image at its base"; exit 1; fi - for f in placed anywhere unplaced; do grep -v '^base ' "$f.txt" > "$f.out"; done - cmp placed.out anywhere.out && cmp placed.out unplaced.out || { echo "::error::an image at its base observes other memory"; exit 1; } + local=$(sed -n 's/^base //p' local.txt) + b=$((local)) + (( b % 65536 == 0 && b >= (1 << 44) && b < (1 << 45) )) \ + || { echo "::error::this boot's base $local is not one the runner chooses"; exit 1; } + grep -qx "base $local" again.txt || { echo "::error::a second start of the boot chose another base"; exit 1; } + ls "$OKRUN_CACHE"/*.okseg > /dev/null || { echo "::error::no data was prepared for this boot's base"; exit 1; } + for f in local placed anywhere unplaced; do grep -v '^base ' "$f.txt" > "$f.out"; done + cmp local.out placed.out && cmp placed.out anywhere.out && cmp placed.out unplaced.out \ + || { echo "::error::an image at its base observes other memory"; exit 1; } for want in "the table holds its text" "the entry is the implementation's" "written two one"; do grep -q "placed: $want" placed.out || { echo "::error::at its base: $want"; exit 1; } done echo " ok an image at its base, anywhere, and unplaced observes the same memory" + - name: A snapshot of the initialisers (0.4.0) + run: | + # The first start runs the initialiser and has a snapshot made beside + # it; the next starts from it. A named value the initialiser read, + # set otherwise, and OKRUN_SNAPSHOT=0 start by the initialiser again; + # an initialiser that asks for the time is refused a snapshot, and the + # marker says so. + a='${{ matrix.arch }}' + export OKRUN_CACHE="$RUNNER_TEMP/okcache" + mkdir -p "$OKRUN_CACHE" + run() { "$OKRUN" "out/$a/$1.oke" | tr -d '\r'; } + expect() { grep -q "$2" <<<"$1" || { echo "::error::$3"; echo "$1"; exit 1; }; } + out=$(run snapshot); expect "$out" 'started by its initialisers' 'the first start ran no initialiser' + run snapshot-timed > /dev/null + for i in $(seq 100); do + n=$(cat "$OKRUN_CACHE"/*.sealing 2>/dev/null | grep -c '^made\|^refused' || true) + [ "$n" -ge 2 ] && break; sleep 0.1 + done + cat "$OKRUN_CACHE"/*.sealing + grep -q '^made' "$OKRUN_CACHE"/*.sealing || { echo "::error::no snapshot was made"; exit 1; } + grep -q 'refused: an initialiser asked for kal_time_monotonic' "$OKRUN_CACHE"/*.sealing \ + || { echo "::error::the initialiser that asked for the time was not refused"; exit 1; } + out=$(run snapshot); echo "$out" + expect "$out" 'started from the snapshot' 'the second start did not start from the snapshot' + expect "$out" 'the memory is filled' 'the snapshot holds other memory' + out=$(OKIMG_SNAPSHOT_NAME=set run snapshot) + expect "$out" 'started by its initialisers' 'a start whose named value differs used the snapshot' + expect "$out" 'the name is set' 'the initialiser read another value' + out=$(OKRUN_SNAPSHOT=0 run snapshot); expect "$out" 'started by its initialisers' 'OKRUN_SNAPSHOT=0 used the snapshot' + out=$(run snapshot-timed); expect "$out" 'started by its initialisers' 'an image refused a snapshot started from one' + echo " ok a snapshot is made once, used, compared, declined and refused" - name: The refusals run: | a='${{ matrix.arch }}' diff --git a/docs/snapshot.md b/docs/snapshot.md new file mode 100644 index 0000000..e39dc7c --- /dev/null +++ b/docs/snapshot.md @@ -0,0 +1,127 @@ +# A snapshot of an image's initialisers + +An image's initialisers --- its C library's start, a C++ program's static constructors --- compute +the same memory at every start of the same image on the same machine. A snapshot keeps that +memory: the runner runs the initialisers once in a boot and writes the image's data and the +memory they obtained to a prepared copy (SPEC 12.4, type 5, flag `OKLD_PREPARED_SNAPSHOT`). +Every later start maps the copy, runs no initialiser, and enters the image's C library to take +what the start itself supplies. The snapshot is the runner's and the user's; it is not +distributed, and nothing in the image as published describes it. + +## When one is made and used + +A runner that places an image at this boot's base (`OKRUN_PLACE` unset: the image states a +placement and the implementation reports `KAL_EXEC_PROP_PLACE`) looks for +`-.oksnap` in its cache directory (`OKRUN_CACHE`, else `$XDG_RUNTIME_DIR/okrun`, +else the user's cache directory's `okrun/placed`). It uses the file when its note states the +image's digest, the base, and the word of facts this implementation gives (below). + +When there is none, and the image's C library takes part (it exports `__okrun_snapshot`), the +runner creates `-.sealing` exclusively and starts the program again as it was +started --- its own image, its arguments, its named values --- with `OKRUN_SNAPSHOT=start` and +the null device for its streams. That runner starts another with `OKRUN_SNAPSHOT=make` and ends +at once, so that the first waits only for it and the one making the snapshot outlives no +parent. The first goes on to start the image as it would have. The marker records the outcome +(`made`, or `refused:` and the reason), and no other start of the boot asks again; a new boot +removes the markers with the copies. A program started for economy (openkal 0.23, +`KAL_RESOURCES_ECONOMY`) uses a snapshot and does not have one made. + +`OKRUN_SNAPSHOT=0` neither uses nor makes one. + +## What the initialisers are given while it is made + +The runner making a snapshot loads the image at the base and binds its imports to +`run/src/seal.cpp`: + +| names | answer | +| --- | --- | +| `kal_alloc`, `kal_free`, `kal_region_*` | memory of the arena; nothing is given back | +| `kal_memory_granularity`, `kal_exec_granularity`, every `*_props` without an argument, `kal_version`, `kal_interfaces`, `kal_time_monotonic_granularity`, `kal_timeout_granularity`, `kal_fs_max_name`, `kal_env_system` | the implementation's own answer: a fact | +| `kal_task_current` | one fixed word, the context the initialisers run on | +| `kal_task_yield` | nothing to yield to | +| `kal_prof_active` | zero | +| every other name | refused: the runner ends without a snapshot, and the marker names the call | + +The facts are what the implementation states of itself. They are folded into one word, which +the copy records and a start compares with its own: a snapshot made under another +implementation, another version of it or another machine's granularity is not used. + +**The arena** lies beside a boundary of 256 MiB after the image's span. A region, and any memory +aligned to 64 KiB or more, is placed at or above the boundary, aligned from the address itself +as asked --- an allocator that finds a region's metadata by masking an address (mimalloc aligns +its regions to 256 MiB) needs the alignment of the address, not of an offset. Other memory is +placed below the boundary, downward, while it does not reach the image, and above the regions +otherwise. The arena is one range, and its addresses are asked of the environment as it grows +(`kal_exec_alloc_at`). + +## The C library's part + +An image's C library takes part by exporting two names: + +```c +struct okrun_snapshot_record { /* __okrun_snapshot */ + kal_u64 arena; /* the arena's address */ + kal_u64 size; /* its bytes */ + kal_u32 phase; /* 1: the initialisers run for a snapshot; 2: a start from one */ + kal_u32 reserved; + kal_u64 context; /* the word of the context the initialisers ran on */ +}; +void __okrun_snapshot_start(void); +``` + +**Phase 1.** The runner sets `phase` to 1 before the initialisers run. The C library brings up +its memory and nothing else: no named value, no stream, no randomness, no clock. A named value +read while the phase lasts (`getenv`) is asked of the start, which is refused. After the +initialisers the runner calls `__okrun_snapshot_start`, and the C library reports, by the same +means, what they did that a snapshot cannot hold --- a named value set by one, which the start's +own would replace. + +**Phase 2.** A start from the snapshot writes the record (`phase` 2, the arena, the context) and +calls `__okrun_snapshot_start` before the image's entry. The C library makes the snapshot's +context record its own context's, takes the arguments, the named values, the streams, the +directories, randomness and the stack's guard, and lets its allocator draw its own randomness +again. Memory within the arena is never given back to the environment, which did not give it: +it is the runner's mapping of the copy. + +openkal-musl 0.27.0 takes part (`port/src/okm_start.c`). + +## The copy + +The note's descriptor (`okld_prepared_info`) states the base, the image's digest, the facts, the +arena and its size, the context word, and the offsets from the base of `__okrun_snapshot_start` +and of `__okrun_snapshot`. Its blocks are the 64 KiB blocks of the image's writable segments and +of the arena that hold anything but zeros, joined where at most two blocks of zeros lie between, +at file offsets congruent to their addresses modulo 65536; a block lies within one segment or +within the arena. + +`okld_load_prepared` reserves the span at the base and the arena after it, maps each block +privately (`KAL_EXEC_MAP_WRITE`, read in with the image where the runner reads ahead), and +applies again only the relocations that name an import --- the words the initialisers may not +have changed, bound now to this process's implementation. The image's initialisers are counted +as run. A snapshot of an image that needs another image is refused: that image would start +without its own. + +## The proof + +Before it writes the copy, the runner holds that the snapshot holds nothing of its own process: + +1. every relocation that names an import still holds what the loader wrote there + (`okld_bound_imports`): an initialiser that wrote over a binding is refused; +2. no other word of the image's data or of the arena is the address of a binding: a function + pointer to an import kept elsewhere would, in a start from the snapshot, be the address of a + refusal of the runner that made it. + +The bindings are the only addresses of the runner's process the initialisers are given. An +address of the stack they keep is of frames that have returned, as it is after any start; the +bytes a copy of a value carries beyond what it holds (a short string's unused characters) are +often such addresses, so they are not examined. + +## What differs from a start without one + +- Options an allocator reads from the named values while the C library starts (`MIMALLOC_*`) + are read after the snapshot's memory exists, and those it read while the initialisers ran do + not take effect; `OKRUN_SNAPSHOT=0` runs the initialisers at the start. +- A program whose initialisers read the named values other than through `getenv` (`environ` + itself) reads them empty while the snapshot is made, and no refusal reports it. +- The allocator's per-heap secrets are drawn once a boot, when the snapshot is made, not once a + start. diff --git a/examples/snapshot/app.c b/examples/snapshot/app.c new file mode 100644 index 0000000..7f8f032 --- /dev/null +++ b/examples/snapshot/app.c @@ -0,0 +1,60 @@ +/* A snapshot of an image's initialisers (openkal-image 0.4, docs/snapshot.md), + * the image's own C library's part written out: the record `__okrun_snapshot' + * and the second phase's entry `__okrun_snapshot_start'. + * + * Its initialiser obtains memory and fills it, records the context it runs on, + * and reads a named value. A start from the snapshot finds the memory filled, + * the context the snapshot's rather than its own, and its second phase + * entered; a start without it runs the initialiser. Built as timed.c, the + * initialiser also asks for the time, which a snapshot cannot hold. */ +typedef unsigned long kal_uintptr; +typedef long kal_intptr; +typedef unsigned long long kal_u64; +typedef unsigned int kal_u32; +struct kal_stream { kal_uintptr h; }; + +struct kal_stream kal_stdout(void); +kal_intptr kal_stream_write(struct kal_stream, const void*, kal_uintptr); +void* kal_alloc(kal_uintptr size, kal_uintptr align); +kal_uintptr kal_task_current(void); +kal_intptr kal_env_var(const char* name, kal_uintptr len, char* out, kal_uintptr cap); +kal_u64 kal_time_monotonic(void); + +struct record { kal_u64 arena, size; kal_u32 phase, reserved; kal_u64 context; }; +__attribute__((visibility("default"), used)) struct record __okrun_snapshot; + +enum { kSize = 300000 }; +static unsigned char* g_built; +static kal_uintptr g_context; +static kal_intptr g_named = -2; +static int g_second; +static kal_u64 g_seen; + +static void put(const char* s) { kal_uintptr n = 0; while (s[n]) ++n; kal_stream_write(kal_stdout(), s, n); } + +__attribute__((constructor)) static void build(void) { + g_built = kal_alloc(kSize, 16); + for (unsigned long i = 0; g_built && i < kSize; ++i) g_built[i] = (unsigned char)(i * 7 + 1); + g_context = kal_task_current(); + char v[32]; + g_named = kal_env_var("OKIMG_SNAPSHOT_NAME", 19, v, sizeof v); +} + +#ifdef ASKS_THE_TIME +__attribute__((constructor(200))) static void timed(void) { g_seen = kal_time_monotonic(); } +#endif + +__attribute__((visibility("default"))) void __okrun_snapshot_start(void) { + if (__okrun_snapshot.phase == 2) g_second = 1; +} + +__attribute__((visibility("default"))) int ok_main(void) { + int filled = g_built != 0; + for (unsigned long i = 0; filled && i < kSize; ++i) filled = g_built[i] == (unsigned char)(i * 7 + 1); + put(filled ? "snapshot: the memory is filled\n" : "snapshot: THE MEMORY IS WRONG\n"); + put(g_second && g_context != kal_task_current() ? "snapshot: started from the snapshot\n" + : !g_second && g_context == kal_task_current() ? "snapshot: started by its initialisers\n" + : "snapshot: THE PHASE AND THE CONTEXT DISAGREE\n"); + put(g_named >= 0 ? "snapshot: the name is set\n" : "snapshot: the name is absent\n"); + return 0; +} diff --git a/examples/snapshot/timed.c b/examples/snapshot/timed.c new file mode 100644 index 0000000..af9aa34 --- /dev/null +++ b/examples/snapshot/timed.c @@ -0,0 +1,4 @@ +/* The image of app.c whose initialiser also asks for the time: no snapshot is + * made of it, and the runner that tried records why. */ +#define ASKS_THE_TIME 1 +#include "app.c" diff --git a/ld/include/okld.h b/ld/include/okld.h index e7156ed..af077dd 100644 --- a/ld/include/okld.h +++ b/ld/include/okld.h @@ -351,6 +351,8 @@ okld_u32 okld_touches(const void* image, okld_size size, okld_u32* out, okld_u32 * loader takes the image's code from the image and these blocks for its data, * mapping each block it can and copying the rest. */ #define OKLD_PREPARED_SNAPSHOT 1u +/* The blocks a prepared copy holds at most. */ +#define OKLD_PREPARED_MAX 256u struct okld_prepared_block { okld_u64 address; /* where the bytes go */ @@ -363,8 +365,14 @@ struct okld_prepared_info { okld_u32 flags; /* OKLD_PREPARED_* */ okld_u64 base; /* the image's base */ unsigned char digest[32]; /* the image, by the runner's name for it (SHA-256) */ - okld_u32 count; /* blocks */ - okld_u32 reserved; + okld_u32 count; /* blocks, at most OKLD_PREPARED_MAX */ + okld_u32 facts; /* snapshot: a word of what the environment answered the + initialisers, which a start compares with its own */ + okld_u64 asked; /* snapshot: which of a start's own answers the initialisers + asked for (the runner's positions), folded into `facts' */ + okld_u64 kept; /* the runner's own bytes kept with the copy: their offset in + the file (0: none), after the blocks */ + okld_u64 kept_size; /* and their size */ okld_u64 arena; /* snapshot: the arena's address, after the image's span */ okld_u64 arena_size; /* snapshot: the bytes of it the state reaches */ okld_u64 context; /* snapshot: the word of the context the initialisers ran on */ @@ -380,10 +388,11 @@ struct okld_prepared_info { okld_u64 okld_prepare(const void* image, okld_size size, okld_u64 base, const unsigned char digest[32], void* out, okld_u64 cap, struct okld_why* why); /* Writes a prepared file of `info' and `blocks', each block's bytes from - * `source(address)' --- the whole of a snapshot's --- as okld_prepare does. */ + * `source(address)' --- the whole of a snapshot's --- and `kept_size' bytes of + * the runner's own from `kept' after them, as okld_prepare does. */ okld_u64 okld_prepare_blocks(const struct okld_prepared_info* info, const struct okld_prepared_block* blocks, const void* (*source)(okld_u64 address, okld_u64 size, void* ctx), void* ctx, - void* out, okld_u64 cap); + const void* kept, okld_u64 kept_size, void* out, okld_u64 cap); /* Reads a prepared file's note: 0, or negative when it is not one. Its blocks, * up to `cap', into `blocks'. */ int okld_prepared(const void* prepared, okld_size size, struct okld_prepared_info* info, @@ -398,13 +407,25 @@ int okld_load(const void* bytes, okld_size size, const char* name, const struct okld_host* host, struct okld_module** out, struct okld_why* why); /* okld_load, the image's data taken from a prepared copy (type 5) of it: at the * copy's base, which the host reserves (reserve_at) or the load is refused; - * with a snapshot, its arena reserved after the span and its initialisers - * counted as run. The copy is a file of its own: `okld_host::map' is given its - * bytes. Its digest is not compared with the image's: the caller named both. - * Version 0.4. */ + * with a snapshot, its arena reserved after the span, its initialisers + * counted as run, and of its relocations only those naming an import applied + * again --- what the initialisers wrote over the rest is the snapshot's. A + * snapshot of an image that needs another is refused: the images it needs + * would start without theirs. The copy is a file of its own: `okld_host::map' + * is given its bytes. Its digest and its facts are not compared with the + * image's and the environment's: the caller named both. Version 0.4. */ int okld_load_prepared(const void* bytes, okld_size size, const char* name, const struct okld_host* host, const void* prepared, okld_size prepared_size, struct okld_module** out, struct okld_why* why); +/* EACH WORD A RELOCATION OF THE MODULE WRITES WITH THE ADDRESS OF A NAME IT + * IMPORTS --- a symbol it does not define --- and the value the loader writes + * there now: the words a load of the module's snapshot writes again, for the + * process it is loaded into, and the only words of a snapshot that may hold + * an address the runner gave. Stops when `each' returns non-zero, and returns + * that. Version 0.4. */ +int okld_bound_imports(struct okld_module* m, int (*each)(void* slot, okld_u64 value, const char* name, void* ctx), + void* ctx); + /* Runs the initialisers of the module and of the modules it needs, needed ones * first, each once. `call' enters a function of the image in the image's * convention (the runner supplies it; see okrun). */ diff --git a/ld/src/load.cpp b/ld/src/load.cpp index f02bcd7..43689cf 100644 --- a/ld/src/load.cpp +++ b/ld/src/load.cpp @@ -33,6 +33,9 @@ struct okld_module { const elf::u32* hash = nullptr; // DT_HASH const elf::u32* gnu_hash = nullptr; // DT_GNU_HASH elf::u64 init = 0, fini = 0, init_array = 0, init_arraysz = 0, fini_array = 0, fini_arraysz = 0; + // The relocation tables, within the mapped image (okld_bound_imports). + elf::u64 rela = ~0ull, relasz = 0, jmprel = ~0ull, pltrelsz = 0; + bool x86 = false; void* debug_entry = nullptr; // the GDB JIT record void* debug_copy = nullptr; okld_size debug_size = 0; @@ -198,11 +201,23 @@ void withdraw(okld_module* m) { m->host->free(e, sizeof(jit_code_entry), m->host->ctx); } +// Whether a relocation's symbol is one the module imports: named, and not +// defined by it. +bool imports(const okld_module* m, elf::u32 index) { + return index != 0 && m->symtab[index].shndx == elf::shn_undef; +} + // EACH RELOCATION WRITES A WRITABLE SEGMENT, or the image is refused: a // segment that is not writable may be the file's own pages (okld_host::map), // and a write the format does not allow (SPEC 12.2: no text relocation) is // refused rather than made wherever its offset points. -int relocate(okld_module* m, const view& v, const okld_host* host, const writable& w, bool placed, okld_why* why) { +// What a load applies: every relocation (anywhere), those naming a symbol (at +// a base, whose bytes hold the relative ones), or those naming an import (a +// snapshot, whose bytes hold what the initialisers made of the rest). +enum class applying { all, named, imports }; + +int relocate(okld_module* m, const view& v, const okld_host* host, const writable& w, applying what, okld_why* why) { + const bool placed = what != applying::all; const elf::u64 b = reinterpret_cast(m->base); const bool x86 = v.eh->machine == elf::em_x86_64; auto symbol_address = [&](elf::u32 index, bool& ok) -> elf::u64 { @@ -234,6 +249,7 @@ int relocate(okld_module* m, const view& v, const okld_host* host, const writabl // AT ITS BASE THE RELATIVE ONES ARE ALREADY IN THE BYTES (SPEC // 12.3, openkal 0.23): what remains names a symbol. if (placed && !names_a_symbol(v, t)) continue; + if (what == applying::imports && !imports(m, elf::r_sym(r[k].info))) continue; bool ok = true; if (x86) { switch (t) { @@ -312,7 +328,7 @@ struct source { const unsigned char* bytes = nullptr; // the file the blocks are in size n = 0; elf::u64 base = 0; - okld_prepared_block block[64]; + okld_prepared_block block[OKLD_PREPARED_MAX]; elf::u32 count = 0; bool snapshot = false; elf::u64 arena = 0, arena_size = 0; @@ -452,7 +468,7 @@ bool placement_source(const view& v, const placement& p, const void* bytes, sour out.n = v.len; out.base = p.base; out.count = 0; - for (elf::u32 i = 0; i < p.count && out.count < 64; ++i) + for (elf::u32 i = 0; i < p.count && out.count < OKLD_PREPARED_MAX; ++i) out.block[out.count++] = okld_prepared_block{ p.base + p.e[i].vaddr, p.e[i].offset, p.e[i].size }; return true; } @@ -492,6 +508,11 @@ int load_image(const void* bytes, okld_size n, const char* name, const okld_host if (dyn[k].tag != elf::dt_needed) continue; const char* need = v.string(dyn[k].val); if (!need) continue; + if (prepared && prepared->snapshot) { + say(why, "a snapshot of an image that needs '", need, "' is refused: that image would start without its own"); + host->free(m, sizeof *m, host->ctx); + return -1; + } if (m->needed_count == 16) { say(why, "the image needs more than sixteen images"); host->free(m, sizeof *m, host->ctx); return -1; } okld_size nsize = 0; const void* nbytes = host->find_needed ? host->find_needed(need, &nsize, host->ctx) : nullptr; @@ -653,8 +674,12 @@ int load_image(const void* bytes, okld_size n, const char* name, const okld_host m->init = v.dynamic(elf::dt_init); m->fini = v.dynamic(elf::dt_fini); m->init_array = v.dynamic(elf::dt_init_array); m->init_arraysz = v.dynamic(elf::dt_init_arraysz); m->fini_array = v.dynamic(elf::dt_fini_array); m->fini_arraysz = v.dynamic(elf::dt_fini_arraysz); + m->rela = v.dynamic(elf::dt_rela, ~0ull); m->relasz = v.dynamic(elf::dt_relasz); + m->jmprel = v.dynamic(elf::dt_jmprel, ~0ull); m->pltrelsz = v.dynamic(elf::dt_pltrelsz); + m->x86 = v.eh->machine == elf::em_x86_64; - if (relocate(m, v, host, w, placed, why) != 0) { discard(m); return -1; } + const applying what = !placed ? applying::all : src->snapshot ? applying::imports : applying::named; + if (relocate(m, v, host, w, what, why) != 0) { discard(m); return -1; } // READ-ONLY AFTER RELOCATION (openkal 0.23): the addresses of functions and // data relocation wrote, made read-only now, as a program of the @@ -703,7 +728,8 @@ extern "C" int okld_load_prepared(const void* bytes, okld_size n, const char* na const void* prepared, okld_size prepared_size, okld_module** out, okld_why* why) { okld_prepared_info info{}; source src; - if (okld_prepared(prepared, prepared_size, &info, src.block, 64) != 0 || info.count > 64) { + if (okld_prepared(prepared, prepared_size, &info, src.block, OKLD_PREPARED_MAX) != 0 + || info.count > OKLD_PREPARED_MAX) { say(why, "not a prepared copy of an image (SPEC 12.4, type 5)"); return -1; } @@ -743,6 +769,33 @@ void run_fini(okld_module* m, void (*call)(void*, void*), void* ctx) { } } // namespace +extern "C" int okld_bound_imports(okld_module* m, int (*each)(void*, okld_u64, const char*, void*), void* ctx) { + if (!m || !each) return 0; + const okld_host* host = m->host; + const elf::u64 tables[2][2] = { { m->rela, m->relasz }, { m->jmprel, m->pltrelsz } }; + for (const auto& t : tables) { + if (t[0] == ~0ull) continue; + const auto* r = reinterpret_cast(m->base + t[0]); + for (elf::u64 k = 0; k < t[1] / sizeof(elf::rela); ++k) { + const elf::u32 index = elf::r_sym(r[k].info); + if (!imports(m, index)) continue; + const auto& sym = m->symtab[index]; + const char* name = sym.name < m->strsz ? m->strtab + sym.name : ""; + elf::u64 value = 0; + if (void* p = host->resolve ? host->resolve(name, host->ctx) : nullptr) value = reinterpret_cast(p); + for (okld_size n = 0; value == 0 && n < m->needed_count; ++n) + if (void* p = lookup(m->needed[n], name)) value = reinterpret_cast(p); + // The addend counts where the relocation adds it: every type but + // x86-64's GLOB_DAT and JUMP_SLOT. + const elf::u32 type = elf::r_type(r[k].info); + const bool adds = !m->x86 || (type != elf::r_x86_64_glob_dat && type != elf::r_x86_64_jump_slot); + if (value != 0 && adds) value += static_cast(r[k].addend); + if (const int stop = each(m->base + r[k].offset, value, name, ctx)) return stop; + } + } + return 0; +} + extern "C" void okld_initialise(okld_module* m, void (*call)(void*, void*), void* ctx) { run_init(m, call, ctx); } extern "C" void okld_finalise(okld_module* m, void (*call)(void*, void*), void* ctx) { run_fini(m, call, ctx); } extern "C" void* okld_symbol(okld_module* m, const char* name) { return lookup(m, name); } diff --git a/ld/src/okld.cppm b/ld/src/okld.cppm index 74e92f9..f13f109 100644 --- a/ld/src/okld.cppm +++ b/ld/src/okld.cppm @@ -26,6 +26,7 @@ export using ::okld_sign; export using ::okld_verify; export using ::okld_public_key; export using ::okld_load; +export using ::okld_bound_imports; export using ::okld_initialise; export using ::okld_finalise; export using ::okld_symbol; @@ -79,4 +80,5 @@ inline constexpr int map_execute = OKLD_MAP_EXECUTE; inline constexpr int map_prefetch = OKLD_MAP_PREFETCH; inline constexpr int map_write = OKLD_MAP_WRITE; inline constexpr okld_u32 prepared_snapshot = OKLD_PREPARED_SNAPSHOT; +inline constexpr okld_u32 prepared_max = OKLD_PREPARED_MAX; } diff --git a/ld/src/place.cpp b/ld/src/place.cpp index e83d4e8..8972d1d 100644 --- a/ld/src/place.cpp +++ b/ld/src/place.cpp @@ -287,36 +287,40 @@ extern "C" okld_u32 okld_touches(const void* image, okld_size n, okld_u32* out, // so that a loader maps it as it maps an image's own file. namespace { -constexpr size prepared_fixed = 4 + 4 + 8 + 32 + 4 + 4 + 8 * 5; // the descriptor before its blocks +constexpr size prepared_fixed = 4 + 4 + 8 + 32 + 4 + 4 + 8 * 8; // the descriptor before its blocks void put_info(unsigned char* d, const okld_prepared_info& i) { size at = 0; auto put = [&](const void* p, size k) { copy(d + at, p, k); at += k; }; - put(&i.version, 4); put(&i.flags, 4); put(&i.base, 8); put(i.digest, 32); put(&i.count, 4); put(&i.reserved, 4); + put(&i.version, 4); put(&i.flags, 4); put(&i.base, 8); put(i.digest, 32); put(&i.count, 4); put(&i.facts, 4); put(&i.asked, 8); put(&i.arena, 8); put(&i.arena_size, 8); put(&i.context, 8); put(&i.start, 8); put(&i.state, 8); + put(&i.kept, 8); put(&i.kept_size, 8); } void get_info(const unsigned char* d, okld_prepared_info& i) { size at = 0; auto get = [&](void* p, size k) { copy(p, d + at, k); at += k; }; - get(&i.version, 4); get(&i.flags, 4); get(&i.base, 8); get(i.digest, 32); get(&i.count, 4); get(&i.reserved, 4); + get(&i.version, 4); get(&i.flags, 4); get(&i.base, 8); get(i.digest, 32); get(&i.count, 4); get(&i.facts, 4); get(&i.asked, 8); get(&i.arena, 8); get(&i.arena_size, 8); get(&i.context, 8); get(&i.start, 8); get(&i.state, 8); + get(&i.kept, 8); get(&i.kept_size, 8); } } // namespace extern "C" okld_u64 okld_prepare_blocks(const okld_prepared_info* info, const okld_prepared_block* blocks, const void* (*source)(okld_u64, okld_u64, void*), void* ctx, - void* out, okld_u64 cap) { - if (!info || (info->count && !blocks) || !source) return 0; + const void* kept, okld_u64 kept_size, void* out, okld_u64 cap) { + if (!info || (info->count && !blocks) || !source || (kept_size && !kept)) return 0; const elf::u64 desc = prepared_fixed + static_cast(info->count) * 24; const elf::u64 header = sizeof(elf::nhdr) + 8 + ((desc + 3) & ~3ull); - // Each block at an offset congruent to its address modulo the alignment. + // Each block at an offset congruent to its address modulo the alignment; + // the runner's bytes after the last, at a multiple of eight. elf::u64 at = header; for (elf::u32 i = 0; i < info->count; ++i) { const elf::u64 offset = at + ((blocks[i].address % alignment) + alignment - (at % alignment)) % alignment; at = offset + blocks[i].size; } - const elf::u64 total = at; + const elf::u64 kept_at = kept_size ? (at + 7) & ~7ull : 0; + const elf::u64 total = kept_size ? kept_at + kept_size : at; if (!out || cap < total) return total; auto* o = static_cast(out); fill(o, 0, total); @@ -324,7 +328,11 @@ extern "C" okld_u64 okld_prepare_blocks(const okld_prepared_info* info, const ok copy(o, &nh, sizeof nh); copy(o + sizeof nh, "OPENKAL", 8); unsigned char* d = o + sizeof nh + 8; - put_info(d, *info); + okld_prepared_info written = *info; + written.kept = kept_at; + written.kept_size = kept_size; + put_info(d, written); + if (kept_size) copy(o + kept_at, kept, kept_size); at = header; for (elf::u32 i = 0; i < info->count; ++i) { const elf::u64 offset = at + ((blocks[i].address % alignment) + alignment - (at % alignment)) % alignment; @@ -379,11 +387,11 @@ extern "C" okld_u64 okld_prepare(const void* image, okld_size n, okld_u64 base, return 0; } file_of f{ &v, static_cast(image), base }; - const okld_u64 total = okld_prepare_blocks(&info, blocks, &file_segment, &f, nullptr, 0); + const okld_u64 total = okld_prepare_blocks(&info, blocks, &file_segment, &f, nullptr, 0, nullptr, 0); if (!out || cap < total) return total; // The segments written where they go, and then relocated there. auto* o = static_cast(out); - if (okld_prepare_blocks(&info, blocks, &file_segment, &f, out, cap) != total) return 0; + if (okld_prepare_blocks(&info, blocks, &file_segment, &f, nullptr, 0, out, cap) != total) return 0; okld_prepared_info back{}; okld_prepared_block placed[8]; if (okld_prepared(o, total, &back, placed, 8) != 0) return 0; @@ -405,6 +413,7 @@ extern "C" int okld_prepared(const void* prepared, okld_size n, okld_prepared_in const unsigned char* d = b + sizeof nh + 8; get_info(d, *info); if (info->version != 1 || prepared_fixed + static_cast(info->count) * 24 > nh.descsz) return -1; + if (info->kept_size && (info->kept > n || info->kept_size > n - info->kept)) return -1; for (elf::u32 i = 0; i < info->count && i < cap; ++i) { copy(&blocks[i], d + prepared_fixed + i * 24, 24); if (blocks[i].offset > n || blocks[i].size > n - blocks[i].offset) return -1; diff --git a/run/mcpp.toml b/run/mcpp.toml index 0742cc8..c54008c 100644 --- a/run/mcpp.toml +++ b/run/mcpp.toml @@ -27,7 +27,7 @@ openkal-windows = "0.19.0" # openkal.space is provided its names are bound, and elsewhere an image that # requires it is refused at load, naming it. [build] -sources = ["src/okrun.cppm", "src/run.cpp", "src/bindings.cpp", "src/prof.cpp"] +sources = ["src/okrun.cppm", "src/run.cpp", "src/bindings.cpp", "src/seal.cpp", "src/prof.cpp"] include_dirs = ["include"] flags = [ { glob = "src/*.cpp", cxxflags = ["-fno-exceptions", "-fno-rtti"] }, diff --git a/run/src/bindings.cpp b/run/src/bindings.cpp index fe37298..7b7ef37 100644 --- a/run/src/bindings.cpp +++ b/run/src/bindings.cpp @@ -102,9 +102,7 @@ OKRUN_IMAGE_ABI kal_intptr own_kal_env_loader(char* out, kal_uintptr cap) { // are answered here, and the word of interfaces states them beside the // implementation's. The program's end is where a measurement writes what it // measured, so `kal_exit' and `kal_abort' pass through here first. -OKRUN_IMAGE_ABI kal_u64 own_kal_interfaces(void) { - return kal_interfaces() | KAL_IFACE_PROF | KAL_IFACE_DEBUG; -} +OKRUN_IMAGE_ABI kal_u64 own_kal_interfaces(void) { return own_interfaces(); } OKRUN_IMAGE_ABI void own_kal_exit(int status) { profile_phase("exit"); profile_finish(); @@ -180,6 +178,8 @@ static_assert(sizeof g_table / sizeof g_table[0] <= kProfiledNames, "the call co } // namespace +kal_u64 own_interfaces() { return kal_interfaces() | KAL_IFACE_PROF | KAL_IFACE_DEBUG; } + const binding* bindings(unsigned long& count) { count = sizeof g_table / sizeof g_table[0]; return profile_calls() ? g_counted : g_table; diff --git a/run/src/bindings_space.cpp b/run/src/bindings_space.cpp index 65a7252..f09a4a1 100644 --- a/run/src/bindings_space.cpp +++ b/run/src/bindings_space.cpp @@ -26,4 +26,7 @@ const binding* space_bindings(unsigned long& count) { return g_space; } +kal_uintptr space_props() { return kal_space_props(); } +void* space_props_binding() { return reinterpret_cast(&present<&::kal_space_props>::call); } + } // namespace okrun diff --git a/run/src/no_space.cpp b/run/src/no_space.cpp index f25c394..410823e 100644 --- a/run/src/no_space.cpp +++ b/run/src/no_space.cpp @@ -5,4 +5,6 @@ namespace okrun { const binding* space_bindings(unsigned long& count) { count = 0; return nullptr; } +kal_uintptr space_props() { return 0; } +void* space_props_binding() { return nullptr; } } // namespace okrun diff --git a/run/src/run.cpp b/run/src/run.cpp index 840fff7..5c11727 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -812,6 +812,7 @@ bool boot_slide(kal_dir dir, unsigned long long& slide) { if (kal_fs_list_next(dir, &iter, name, sizeof name, &n, &kind) != kal_ok || iter == 0) break; if (n > 6 && n < sizeof name && same_bytes(name + n - 6, ".okseg", 6)) kal_fs_remove(dir, name, n); if (n > 7 && n < sizeof name && same_bytes(name + n - 7, ".oksnap", 7)) kal_fs_remove(dir, name, n); + if (n > 8 && n < sizeof name && same_bytes(name + n - 8, ".sealing", 8)) kal_fs_remove(dir, name, n); } } char line[96]; @@ -879,63 +880,460 @@ bool image_digest(search& s, const void* bytes, unsigned long long size, unsigne return true; } -// The prepared copy of the entry image for this boot's base, found or made, -// mapped into `s.prepared'. False leaves the image to its sealed base. -bool prepare_local(search& s, const void* bytes, unsigned long long size) { +// WHERE AN IMAGE IS PLACED ON THIS MACHINE AT THIS BOOT, and the names of what +// is prepared for it there: `.okseg' (its data for the base), +// `.oksnap' (its snapshot) and `.sealing' (a snapshot was asked +// for), the stem being the image's digest and the base. +struct local_place { + owned_dir dir; + unsigned long long base = 0; + unsigned char digest[32] = {}; + char stem[64] = {}; +}; + +bool local_place_of(search& s, const void* bytes, unsigned long long size, local_place& lp) { if (placement_policy() != policy::local || !s.host.reserve_at) return false; okld_u64 sealed = 0; okld_u32 count = 0; if (okld_placement(bytes, size, &sealed, &count) != 1 || count == 0) return false; - unsigned char digest[32]; - if (!image_digest(s, bytes, size, digest)) return false; - owned_dir dir; - if (!cache_directory(dir)) return false; + if (!image_digest(s, bytes, size, lp.digest)) return false; + if (!cache_directory(lp.dir)) return false; unsigned long long slide = 0; - if (!boot_slide(dir.d, slide)) return false; + if (!boot_slide(lp.dir.d, slide)) return false; // From the digest's own base, as okld_place_base chooses one, moved by the // slide within [16 TiB, 32 TiB - 4 GiB): room for a span of 4 GiB above. - const unsigned long long slot = (static_cast(digest[0]) | static_cast(digest[1]) << 8) & 0xfff; + const unsigned long long slot = (static_cast(lp.digest[0]) | static_cast(lp.digest[1]) << 8) & 0xfff; const unsigned long long range = (1ull << 44) - (1ull << 32); - const unsigned long long base = (1ull << 44) + (slot * (1ull << 32) + slide) % range; + lp.base = (1ull << 44) + (slot * (1ull << 32) + slide) % range; + char h[33], b[17]; + to_hex(lp.digest, 16, h); + char* o = b; + for (int sh = 60; sh >= 0; sh -= 4) *o++ = hex_digits[(lp.base >> sh) & 15]; + *o = 0; + text(lp.stem, sizeof lp.stem) << h << "-" << b; + return true; +} + +void place_name(const local_place& lp, const char* suffix, char (&out)[96]) { text(out, sizeof out) << lp.stem << suffix; } + +// A prepared file of the place, mapped, its note read and held to `accept'. +bool map_prepared(const local_place& lp, const char* suffix, file_bytes& out, okld_prepared_info& info, + bool (*accept)(const okld_prepared_info&, const local_place&)) { char name[96]; - { - char h[33], b[17]; - to_hex(digest, 16, h); - char* o = b; - for (int sh = 60; sh >= 0; sh -= 4) *o++ = hex_digits[(base >> sh) & 15]; - *o = 0; - text(name, sizeof name) << h << "-" << b << ".okseg"; + place_name(lp, suffix, name); + kal_file f{}; + if (kal_fs_open(lp.dir.d, name, length(name), KAL_OPEN_READ, &f) != kal_ok) return false; + file_bytes fb; + if (!map_all(f, fb)) { kal_fs_close_file(f); return false; } + okld_prepared_block none[1]; + if (okld_prepared(fb.data, fb.size, &info, none, 0) != 0 || info.base != lp.base + || !same_bytes(info.digest, lp.digest, 32) || !accept(info, lp)) { + release_file(fb); + return false; } - auto found = [&]() -> bool { - kal_file f{}; - if (kal_fs_open(dir.d, name, length(name), KAL_OPEN_READ, &f) != kal_ok) return false; - file_bytes fb; - if (!map_all(f, fb)) { kal_fs_close_file(f); return false; } - okld_prepared_info info{}; - okld_prepared_block blocks[1]; - if (okld_prepared(fb.data, fb.size, &info, blocks, 0) != 0 || info.base != base || info.flags != 0 - || !same_bytes(info.digest, digest, 32)) { - release_file(fb); - return false; - } - s.prepared = fb; - return true; - }; - if (found()) return true; + out = fb; + return true; +} + +// The prepared copy of the entry image for this boot's base, found or made, +// mapped into `s.prepared'. False leaves the image to its sealed base. +bool prepare_local(search& s, const void* bytes, unsigned long long size, const local_place& lp) { + okld_prepared_info info{}; + auto plain = [](const okld_prepared_info& i, const local_place&) { return i.flags == 0; }; + if (map_prepared(lp, ".okseg", s.prepared, info, plain)) return true; okld_why why{}; - const okld_u64 need = okld_prepare(bytes, size, base, digest, nullptr, 0, &why); + const okld_u64 need = okld_prepare(bytes, size, lp.base, lp.digest, nullptr, 0, &why); if (need == 0) return false; void* buffer = kal_alloc(need, 16); if (!buffer) return false; - const bool made = okld_prepare(bytes, size, base, digest, buffer, need, &why) == need - && write_whole(dir.d, name, buffer, need); + char name[96]; + place_name(lp, ".okseg", name); + const bool made = okld_prepare(bytes, size, lp.base, lp.digest, buffer, need, &why) == need + && write_whole(lp.dir.d, name, buffer, need); kal_free(buffer, need, 16); - return made && found(); + return made && map_prepared(lp, ".okseg", s.prepared, info, plain); +} + +// --- a snapshot of the image's initialisers (0.4, docs/snapshot.md) ----------------- +// +// THE INITIALISERS RUN ONCE IN A BOOT, NOT AT EVERY START. A runner that finds no +// snapshot of the image for this boot's base starts the program again to make +// one (`start_making'), and goes on to start the image as it would have; the +// runner it started runs the image's initialisers alone --- with memory, and +// with nothing of the start's own (src/seal.cpp) --- and writes the image's +// data and the memory they obtained as a prepared copy, `.oksnap'. A +// later start maps it, its imports bound again for this process, and enters +// the image's C library to take what the start supplies --- its arguments, its +// named values, its streams --- before its entry. An image whose C library does +// not take part (its `__okrun_snapshot' record), one that needs another image, +// and one whose initialisers ask for anything of the start's own, are not made +// a snapshot of; the marker `.sealing' records why, and the attempt is not +// repeated in the boot. +// +// OKRUN_SNAPSHOT=0 no snapshot is used or made +// +// and, between runners, `start' (start the runner that makes it, and end) and +// `make' (make it). +enum class snapshotting { use, off, start, make }; + +snapshotting snapshot_mode() { + char v[8]; + const kal_intptr n = kal_env_var("OKRUN_SNAPSHOT", 14, v, sizeof v); + if (n == 1 && v[0] == '0') return snapshotting::off; + if (n == 5 && same_bytes(v, "start", 5)) return snapshotting::start; + if (n == 4 && same_bytes(v, "make", 4)) return snapshotting::make; + return snapshotting::use; +} + +// The record an image's C library exports for the runner, and the entry of its +// second phase (docs/snapshot.md). +struct snapshot_record { + kal_u64 arena; // the arena's address + kal_u64 size; // its bytes + kal_u32 phase; // 1: the initialisers run for a snapshot; 2: a start from one + kal_u32 reserved; + kal_u64 context; // the word of the context the initialisers ran on +}; +constexpr char kSnapshotRecord[] = "__okrun_snapshot"; +constexpr char kSnapshotStart[] = "__okrun_snapshot_start"; + +// The snapshot of this boot's base, mapped into `s.prepared', when one was made +// under this implementation's facts. +bool snapshot_found(search& s, const local_place& lp, okld_prepared_info& info) { + auto made_here = [](const okld_prepared_info& i, const local_place&) { + return (i.flags & OKLD_PREPARED_SNAPSHOT) != 0 && i.facts == environment_facts(i.asked) && i.start != 0 + && i.state != 0; + }; + if (!map_prepared(lp, ".oksnap", s.prepared, info, made_here)) return false; + if (info.kept_size && !seal_kept_holds(s.prepared.data + info.kept, info.kept_size)) { + release_file(s.prepared); + return false; + } + return true; +} + +// This program started again, as it was started --- its own image, its +// arguments, its named values --- with OKRUN_SNAPSHOT set to `value' and the +// null device for its streams, where the implementation gives one. +bool start_again(const char* value, kal_process& out) { + char self[1024]; + const kal_intptr sn = kal_env_image(self, sizeof self - 1); + if (sn <= 0 || sn >= static_cast(sizeof self)) return false; + self[sn] = 0; + kal_dir at{}; const char* rest = nullptr; + kal_dir work{}; char wn[8]; kal_uintptr wl = 0; + if (!locate(self, at, rest) || kal_fs_preopen(0, &work, wn, sizeof wn, &wl) != kal_ok) return false; + // The arguments and the named values, in one buffer. + constexpr unsigned kMost = 256; + static char store[65536]; + static const char* argv[kMost]; + static kal_uintptr argl[kMost]; + static const char* envp[kMost + 1]; + static kal_uintptr envl[kMost + 1]; + unsigned long used = 0; + auto keep = [&](kal_intptr n, auto fill) -> const char* { + if (n < 0 || used + static_cast(n) + 1 > sizeof store) return nullptr; + char* p = store + used; + if (fill(p, static_cast(n)) != n) return nullptr; + p[n] = 0; + used += static_cast(n) + 1; + return p; + }; + const kal_uintptr argc = kal_env_arg_count(); + if (argc == 0 || argc > kMost) return false; + for (kal_uintptr i = 0; i < argc; ++i) { + const kal_intptr n = kal_env_arg(i, nullptr, 0); + argv[i] = keep(n, [&](char* p, kal_uintptr c) { return kal_env_arg(i, p, c); }); + if (!argv[i]) return false; + argl[i] = static_cast(n); + } + kal_uintptr envc = 0; + const kal_uintptr names = kal_env_var_count(); + for (kal_uintptr i = 0; i < names && envc < kMost; ++i) { + char name[256]; + const kal_intptr nn = kal_env_var_at(i, name, sizeof name - 1); + if (nn <= 0 || nn >= static_cast(sizeof name)) continue; + name[nn] = 0; + if (nn == 14 && same_bytes(name, "OKRUN_SNAPSHOT", 14)) continue; + const kal_intptr vn = kal_env_var(name, static_cast(nn), nullptr, 0); + if (vn < 0 || used + static_cast(nn + vn) + 2 > sizeof store) continue; + char* p = store + used; + for (kal_intptr k = 0; k < nn; ++k) p[k] = name[k]; + p[nn] = '='; + if (kal_env_var(name, static_cast(nn), p + nn + 1, static_cast(vn)) != vn) continue; + p[nn + 1 + vn] = 0; + used += static_cast(nn + vn) + 2; + envp[envc] = p; + envl[envc++] = static_cast(nn + vn + 1); + } + char mine[32]; + text(mine, sizeof mine) << "OKRUN_SNAPSHOT=" << value; + envp[envc] = mine; + envl[envc++] = length(mine); + const kal_uintptr quiet = (kal_process_props() & KAL_PROCESS_PROP_NULL_STREAMS) + ? (KAL_SPAWN_NULL_IN | KAL_SPAWN_NULL_OUT | KAL_SPAWN_NULL_ERR) : 0; + const kal_spawn how{ at, work, nullptr, nullptr, 0, quiet }; + return kal_process_spawn(&how, rest, length(rest), argv, argl, argc, envp, envl, envc, nullptr, &out) == kal_ok; +} + +// ONCE IN A BOOT FOR AN IMAGE: the marker is made exclusively, and only the +// start that made it starts the runner that makes the snapshot. Not for a +// program that asked to spend nothing ahead of it (openkal 0.23, economy). +void start_making(const local_place& lp) { + if (kal_env_resources() == KAL_RESOURCES_ECONOMY) return; + char name[96]; + place_name(lp, ".sealing", name); + kal_file f{}; + if (kal_fs_open(lp.dir.d, name, length(name), KAL_OPEN_WRITE | KAL_OPEN_CREATE | KAL_OPEN_EXCLUSIVE, &f) != kal_ok) + return; + kal_fs_close_file(f); + kal_process p{}; + if (!start_again("start", p)) return; + int status = 0, terminated = 0; + kal_process_wait(p, &status, &terminated); + kal_process_close(p); } using image_function = void (OKRUN_IMAGE_ABI*)(void); using image_main = int (OKRUN_IMAGE_ABI*)(void); void call_image(void* fn, void*) { reinterpret_cast(fn)(); } +// --- the runner that makes a snapshot ------------------------------------------------ + +constexpr kal_u64 kBlock = 65536; + +struct program_header { kal_u32 type, flags; kal_u64 offset, vaddr, paddr, filesz, memsz, align; }; +constexpr kal_u32 kLoad = 1, kWritable = 2; + +// The module's loadable segments, as `each(from, to)' of its addresses, the +// writable ones or all of them. +struct segments_of { okld_module* m; bool writable_only; void (*each)(kal_u64, kal_u64, void*); void* ctx; }; +void for_segments(okld_module* m, bool writable_only, void (*each)(kal_u64, kal_u64, void*), void* ctx) { + segments_of w{ m, writable_only, each, ctx }; + okld_iterate([](void* base, const void* phdr, okld_u32 phnum, const char*, void* c) -> int { + auto& w = *static_cast(c); + if (base != okld_base(w.m)) return 0; + const auto* ph = static_cast(phdr); + const auto b = reinterpret_cast(base); + for (okld_u32 i = 0; i < phnum; ++i) + if (ph[i].type == kLoad && (!w.writable_only || (ph[i].flags & kWritable))) + w.each(b + ph[i].vaddr, b + ph[i].vaddr + ph[i].memsz, w.ctx); + return 1; + }, &w); +} + +// What the runner making a snapshot reports when it ends without one: the +// marker of the place says why. +local_place* g_making = nullptr; + +// A RUN OF BLOCKS: the 64 KiB blocks of a range that hold anything but zeros, +// joined where at most two blocks of zeros lie between, as a prepared copy's +// blocks (okld_prepare_blocks); and, where they are more than a copy holds, the +// two nearest joined until they are not. +struct block_list { + okld_prepared_block b[OKLD_PREPARED_MAX * 4]; + kal_u32 count = 0; + bool overflow = false; +}; +bool zeros(kal_u64 from, kal_u64 to) { + const auto* p = reinterpret_cast(from); + for (kal_u64 i = 0; i < (to - from) / 8; ++i) if (p[i]) return false; + for (kal_u64 a = from + (to - from) / 8 * 8; a < to; ++a) if (*reinterpret_cast(a)) return false; + return true; +} +void add_runs(kal_u64 from, kal_u64 to, void* ctx) { + auto& l = *static_cast(ctx); + kal_u64 run_from = 0, run_to = 0; + bool open = false; + for (kal_u64 c = from; c < to;) { + const kal_u64 e = (c / kBlock + 1) * kBlock < to ? (c / kBlock + 1) * kBlock : to; + if (!zeros(c, e)) { + if (open && c - run_to <= 2 * kBlock) run_to = e; + else { + if (open) { if (l.count == sizeof l.b / sizeof l.b[0]) { l.overflow = true; return; } l.b[l.count++] = { run_from, 0, run_to - run_from }; } + run_from = c; run_to = e; open = true; + } + } + c = e; + } + if (open) { if (l.count == sizeof l.b / sizeof l.b[0]) { l.overflow = true; return; } l.b[l.count++] = { run_from, 0, run_to - run_from }; } +} +// Runs of one range are joined, never runs of two: a block lies within one +// segment, or within the arena. +void fit(block_list& l, kal_u64* range_of) { + while (l.count > OKLD_PREPARED_MAX) { + kal_u32 best = 0; kal_u64 gap = ~0ull; + for (kal_u32 i = 0; i + 1 < l.count; ++i) { + if (range_of[i] != range_of[i + 1]) continue; + const kal_u64 g = l.b[i + 1].address - (l.b[i].address + l.b[i].size); + if (g < gap) { gap = g; best = i; } + } + if (gap == ~0ull) { l.overflow = true; return; } + l.b[best].size = l.b[best + 1].address + l.b[best + 1].size - l.b[best].address; + for (kal_u32 i = best + 1; i + 1 < l.count; ++i) { l.b[i] = l.b[i + 1]; range_of[i] = range_of[i + 1]; } + --l.count; + } +} + +// THE PROOF THAT THE SNAPSHOT HOLDS NOTHING OF THIS PROCESS'S OWN: every word +// of the image's data and of the arena that is the address of a binding is +// one a relocation of an import writes (okld_bound_imports), holding what the +// loader wrote there --- a start writes those words again for itself, and a +// binding's address kept anywhere else would call this runner's refusal, or +// nothing, in the process that maps the snapshot. The bindings are this +// process's only addresses the initialisers are given; an address of the +// stack they keep is of frames that have returned, as it is at any start, and +// the bytes a copy carries beyond what it holds --- a short string's unused +// characters --- are often such addresses, so those are not taken for one. +struct import_slot { kal_u64 slot, value; const char* name; }; +struct proof { + import_slot* slots = nullptr; + kal_u32 count = 0, cap = 0; + const char* failure = nullptr; + char why[256] = {}; +}; +bool is_slot(const proof& p, kal_u64 at) { + kal_u32 a = 0, b = p.count; + while (a < b) { const kal_u32 k = (a + b) / 2; if (p.slots[k].slot < at) a = k + 1; else b = k; } + return a < p.count && p.slots[a].slot == at; +} +void hex_of(kal_u64 v, char (&out)[19]) { + out[0] = '0'; out[1] = 'x'; + for (int i = 0; i < 16; ++i) out[2 + i] = hex_digits[(v >> (60 - 4 * i)) & 15]; + out[18] = 0; +} +void prove_range(kal_u64 from, kal_u64 to, void* ctx) { + auto& p = *static_cast(ctx); + if (p.failure) return; + for (kal_u64 a = (from + 7) & ~7ull; a + 8 <= to; a += 8) { + const kal_u64 w = *reinterpret_cast(a); + if (!seal_is_binding(static_cast(w)) || is_slot(p, a)) continue; + char at[19], value[19]; + hex_of(a, at); + hex_of(w, value); + text(p.why, sizeof p.why) << "an initialiser kept the address of a binding, " << value << ", at " << at; + p.failure = p.why; + return; + } +} + +[[noreturn]] void make_snapshot(search& s, const void* bytes, unsigned long long size, const char* name) { + local_place lp; + if (!local_place_of(s, bytes, size, lp)) seal_refused("the image is not placed at a base of this boot"); + g_making = &lp; + if (!prepare_local(s, bytes, size, lp)) seal_refused("its data for this boot's base cannot be prepared"); + s.host.resolve = [](const char* n, void*) { return seal_resolve(n); }; + s.host.debugger = 0; + s.host.prefetch = 0; + okld_module* m = nullptr; + okld_why reason{}; + if (okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason) != 0) + seal_refused(reason.text); + release_ran(s); + int modules = 0; + okld_iterate([](void*, const void*, okld_u32, const char*, void* c) { ++*static_cast(c); return 0; }, &modules); + if (modules != 1) seal_refused("the image needs another, which would start without its own snapshot"); + auto* record = static_cast(okld_symbol(m, kSnapshotRecord)); + void* second = okld_symbol(m, kSnapshotStart); + if (!record || !second) seal_refused("the image's C library takes no part in a snapshot (__okrun_snapshot)"); + // The arena, after the span. + kal_u64 span_end = 0; + for_segments(m, false, [](kal_u64, kal_u64 to, void* c) { auto& e = *static_cast(c); if (to > e) e = to; }, &span_end); + seal_begin(span_end); + *record = snapshot_record{ 0, 0, 1, 0, kSealContext }; + okld_initialise(m, &call_image, nullptr); + // THE SECOND PHASE'S ENTRY, IN THE FIRST: the C library reports what its + // initialisers did that a snapshot cannot hold, by asking for what the + // start supplies, which is refused. + call_image(second, nullptr); + kal_u64 arena_at = 0, arena_end = 0; + seal_arena(arena_at, arena_end); + const kal_u64 used = arena_end - arena_at; + + proof p; + okld_bound_imports(m, [](void*, okld_u64, const char*, void* c) { ++static_cast(c)->cap; return 0; }, &p); + p.slots = p.cap ? static_cast(kal_alloc(p.cap * sizeof(import_slot), 16)) : nullptr; + if (p.cap && !p.slots) seal_refused("no memory for the proof"); + okld_bound_imports(m, [](void* slot, okld_u64 value, const char* n, void* c) { + auto& p = *static_cast(c); + if (p.count < p.cap) p.slots[p.count++] = { reinterpret_cast(slot), value, n }; + return 0; + }, &p); + for (kal_u32 i = 1; i < p.count; ++i) // in order of address + for (kal_u32 j = i; j > 0 && p.slots[j - 1].slot > p.slots[j].slot; --j) { + const import_slot t = p.slots[j]; p.slots[j] = p.slots[j - 1]; p.slots[j - 1] = t; + } + for (kal_u32 i = 0; i < p.count; ++i) + if (*reinterpret_cast(p.slots[i].slot) != p.slots[i].value) { + text(p.why, sizeof p.why) << "an initialiser wrote over the binding of " << p.slots[i].name; + seal_refused(p.why); + } + for_segments(m, true, &prove_range, &p); + prove_range(arena_at, arena_at + used, &p); + if (p.failure) seal_refused(p.failure); + + // The blocks: the image's data and the arena, where they are not zero. + static block_list l; + static kal_u64 range_of[sizeof l.b / sizeof l.b[0]]; + struct segment_runs { block_list* l; kal_u64* range_of; kal_u64 range; }; + segment_runs r{ &l, range_of, 0 }; + for_segments(m, true, [](kal_u64 from, kal_u64 to, void* c) { + auto& r = *static_cast(c); + const kal_u32 before = r.l->count; + add_runs(from, to, r.l); + for (kal_u32 i = before; i < r.l->count; ++i) r.range_of[i] = r.range; + ++r.range; + }, &r); + { + const kal_u32 before = l.count; + add_runs(arena_at, arena_at + used, &l); + for (kal_u32 i = before; i < l.count; ++i) range_of[i] = ~0ull; + } + fit(l, range_of); + if (l.overflow) seal_refused("its data and its arena are more blocks than a prepared copy holds"); + + okld_prepared_info info{}; + info.version = 1; + info.flags = OKLD_PREPARED_SNAPSHOT; + info.base = lp.base; + for (int i = 0; i < 32; ++i) info.digest[i] = lp.digest[i]; + info.count = l.count; + info.asked = seal_asked(); + info.facts = environment_facts(info.asked); + info.arena = arena_at; + info.arena_size = used; + info.context = kSealContext; + info.start = reinterpret_cast(second) - lp.base; + info.state = reinterpret_cast(record) - lp.base; + auto live = [](okld_u64 address, okld_u64, void*) -> const void* { return reinterpret_cast(address); }; + static unsigned char kept[65536]; + const kal_u64 kept_size = seal_kept(kept, sizeof kept); + if (kept_size > sizeof kept) seal_refused("the named values asked for are more than a snapshot keeps"); + const okld_u64 total = okld_prepare_blocks(&info, l.b, live, nullptr, kept, kept_size, nullptr, 0); + void* out = total ? kal_alloc(total, 16) : nullptr; + if (!out) seal_refused("no memory for the snapshot's file"); + // The record as a start finds it, before it writes its own phase. + *record = snapshot_record{ arena_at, used, 0, 0, kSealContext }; + char file[96]; + place_name(lp, ".oksnap", file); + if (okld_prepare_blocks(&info, l.b, live, nullptr, kept, kept_size, out, total) != total + || !write_whole(lp.dir.d, file, out, total)) + seal_refused("its file cannot be written"); + char marker[96]; + place_name(lp, ".sealing", marker); + char line[64]; + text(line, sizeof line) << "made\n"; + write_whole(lp.dir.d, marker, line, length(line)); + kal_exit(0); +} + +// The runner between: it starts the one that makes the snapshot, and ends. +void start_maker_and_end() { + if (snapshot_mode() != snapshotting::start) return; + kal_process p{}; + if (start_again("make", p)) kal_process_close(p); + kal_exit(0); +} + // Loads an admitted image with the search, and enters it. Returns only when // it could not be started. int enter(search& s, const void* bytes, unsigned long long size, const char* name, @@ -947,13 +1345,25 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam state().carried = s.options && s.options->carried; state().started = true; prepare_host(s, true); + const snapshotting snapshot = snapshot_mode(); + if (snapshot == snapshotting::make) make_snapshot(s, bytes, size, name); profile_phase("admitted"); okld_module* m = nullptr; okld_why reason{}; - // AT THIS BOOT'S BASE WHERE THE RUNNER PREPARED IT, at the base it was - // sealed with otherwise, and anywhere where neither can be had (0.4). + // FROM THE SNAPSHOT OF THIS BOOT'S BASE, where there is one; at that base + // where the runner prepared the image's data for it, at the base it was + // sealed with otherwise, and anywhere where none can be had (0.4). int loaded = -1; - if (prepare_local(s, bytes, size)) { + bool from_snapshot = false; + okld_prepared_info snap{}; + local_place lp; + const bool local = local_place_of(s, bytes, size, lp); + if (local && snapshot == snapshotting::use && snapshot_found(s, lp, snap)) { + loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason); + if (loaded == 0) from_snapshot = true; + else { release_file(s.prepared); reason = okld_why{}; } + } + if (loaded != 0 && local && prepare_local(s, bytes, size, lp)) { loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason); if (loaded != 0) { release_file(s.prepared); reason = okld_why{}; } } @@ -977,8 +1387,19 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam profile_phase("loaded"); profile_settle(); release_ran(s); + // An image whose C library takes part, at this boot's base and without a + // snapshot, has one made for the starts after this one. + if (local && snapshot == snapshotting::use && !from_snapshot && okld_symbol(m, kSnapshotRecord)) + start_making(lp); profile_begin_run(); - okld_initialise(m, &call_image, nullptr); + if (from_snapshot) { + // THE SECOND PHASE: the C library takes what this start supplies. + auto* base = static_cast(okld_base(m)); + *reinterpret_cast(base + snap.state) = snapshot_record{ snap.arena, snap.arena_size, 2, 0, snap.context }; + call_image(base + snap.start, nullptr); + } else { + okld_initialise(m, &call_image, nullptr); + } profile_phase("initialised"); const int status = reinterpret_cast(entry)(); profile_phase("main"); @@ -1066,6 +1487,17 @@ search g_search; image_state& state() { return g_state; } +// The runner making a snapshot ends without one, and its place's marker says why. +void seal_refused(const char* what) { + if (g_making) { + char marker[96], line[512]; + place_name(*g_making, ".sealing", marker); + text(line, sizeof line) << "refused: " << what << "\n"; + write_whole(g_making->dir.d, marker, line, length(line)); + } + kal_exit(3); +} + bool locate_path(const char* path, kal_dir& dir, const char*& rest) { return locate(path, dir, rest); } } // namespace okrun @@ -1107,6 +1539,7 @@ extern "C" void* okrun_resolve(const char* name) { extern "C" int okrun_run_bytes(const void* bytes, unsigned long long size, const char* name, int argc, const char* const* argv, const okrun_options* options, char* why, unsigned long long cap) { + start_maker_and_end(); search& s = g_search; s.options = options; if (options && options->carried) program_directory(s.directory, sizeof s.directory); @@ -1121,6 +1554,7 @@ extern "C" int okrun_run_bytes(const void* bytes, unsigned long long size, const extern "C" int okrun_run_file(const char* path, int argc, const char* const* argv, const okrun_options* options, char* why, unsigned long long cap) { + start_maker_and_end(); file_bytes f; if (!read_all(path, f, why, cap)) return -1; g_search.ran = f; @@ -1133,6 +1567,7 @@ extern "C" int okrun_run_file(const char* path, int argc, const char* const* arg extern "C" int okrun_run_okb_bytes(const void* bytes, unsigned long long size, const char* entry, int argc, const char* const* argv, const okrun_options* options, char* why, unsigned long long cap) { + start_maker_and_end(); search& s = g_search; s.options = options; if (!is_archive(bytes, size)) { text(why, cap) << "the bytes are not an archive of images (docs/okb.md)"; return -1; } @@ -1147,6 +1582,7 @@ extern "C" int okrun_run_okb_bytes(const void* bytes, unsigned long long size, c extern "C" int okrun_run_images(const okrun_image* images, unsigned long count, const char* entry, int argc, const char* const* argv, const okrun_options* options, char* why, unsigned long long cap) { + start_maker_and_end(); search& s = g_search; s.options = options; s.images = images; diff --git a/run/src/runner.h b/run/src/runner.h index 77e9cd6..3c494b5 100644 --- a/run/src/runner.h +++ b/run/src/runner.h @@ -53,6 +53,38 @@ kal_intptr symbolize(kal_uintptr address, char* out, kal_uintptr cap, kal_uintpt // prefix of it, and the rest (src/run.cpp). bool locate_path(const char* path, kal_dir& dir, const char*& rest); const binding* space_bindings(unsigned long& count); +// openkal.space's word of properties and its binding where it is bound; zero +// and null where it is not (src/bindings_space.cpp, src/no_space.cpp). +kal_uintptr space_props(); +void* space_props_binding(); +// The word of interfaces an image is told: the implementation's, and the +// loader's own (openkal.prof, openkal.debug; SPEC 12.3). +kal_u64 own_interfaces(); + +// --- a snapshot of an image's initialisers (0.4, src/seal.cpp) --------------- +// +// The bindings while they run: memory from an arena above the image's end +// (`seal_begin'), the implementation's facts, `kSealContext' as the one +// context, and every other name a refusal, which ends the runner making the +// snapshot with the name it was asked (`seal_refused', src/run.cpp). +inline constexpr kal_uintptr kSealContext = static_cast(0x5ea1ed0000000001ull); +[[noreturn]] void seal_refused(const char* what); +void seal_begin(kal_u64 image_end); +// The arena's addresses, [lo, hi): all the environment gave it. +void seal_arena(kal_u64& lo, kal_u64& hi); +void* seal_resolve(const char* name); +// Whether a word is the address of one of the bindings above. +bool seal_is_binding(kal_uintptr word); +// Which of a start's own answers the initialisers asked for, and what the +// implementation states of itself with those answers, as one word a snapshot +// records. +kal_u64 seal_asked(); +kal_u32 environment_facts(kal_u64 asked); +// The named values the initialisers asked for, with a word of each answer, as +// bytes kept with the snapshot (`out' null: their size); and whether this start +// answers each the same. +kal_u64 seal_kept(void* out, kal_u64 cap); +bool seal_kept_holds(const void* kept, kal_u64 size); inline bool same(const char* a, const char* b) { for (;; ++a, ++b) { if (*a != *b) return false; if (!*a) return true; } diff --git a/run/src/seal.cpp b/run/src/seal.cpp new file mode 100644 index 0000000..a3b8378 --- /dev/null +++ b/run/src/seal.cpp @@ -0,0 +1,371 @@ +// THE BINDINGS WHILE A SNAPSHOT'S INITIALISERS RUN (0.4, docs/snapshot.md). +// +// A snapshot is an image's data, and the memory it obtained, as its initialisers +// left them, so that every later start maps them and runs no initialiser. What +// the initialisers may be given is what is the same at every start: memory, and +// what the implementation states of itself --- its granularities, its +// properties, its interfaces, its system's name --- which a start compares with +// the word the snapshot records (`environment_facts'). Everything else is the +// start's own: its arguments and named values, its streams and files, the time, +// randomness, other contexts. An initialiser that asks for any of it is refused, +// and no snapshot is made of the image: the refusal ends the runner that was +// making it (`seal_refused', src/run.cpp), and the program itself, which runs +// beside, never meets it. +#include +#include "image_abi.h" +#include "runner.h" + +namespace okrun { + +namespace { + +// THE ARENA: what the initialisers obtain, where every start that maps the +// snapshot finds it. Its addresses lie beside a boundary of 256 MiB after the +// image's span: a region, and any memory aligned to 64 KiB or more, at or above +// the boundary, aligned from the address itself as asked (an allocator aligns +// its regions so: mimalloc to 256 MiB); other memory below it, downward, while +// it does not reach the image, and above the regions otherwise. So the arena is +// one range with no hole an alignment opened before its first region, and the +// environment is asked for its addresses as it grows (kal_exec_alloc_at). +constexpr kal_u64 kPivot = 256ull << 20; +constexpr kal_u64 kMost = 1ull << 30; +constexpr kal_u64 kGrain = 65536; +kal_u64 g_floor = 0; // the image's end: the arena lies above it +kal_u64 g_down = 0, g_up = 0; // the next address below the boundary, and above +kal_u64 g_lo = 0, g_hi = 0; // what the environment gave + +kal_u64 round_up(kal_u64 v, kal_u64 a) { return (v + a - 1) & ~(a - 1); } +kal_u64 round_down(kal_u64 v, kal_u64 a) { return v & ~(a - 1); } + +[[noreturn]] void exhausted(kal_uintptr size, kal_uintptr align) { + static char why[200]; + char* o = why; + auto put = [&](const char* t) { while (*t && o < why + sizeof why - 1) *o++ = *t++; }; + auto num = [&](kal_u64 v) { char d[24]; int n = 0; do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); while (n && o < why + sizeof why - 1) *o++ = d[--n]; }; + put("the initialisers asked for "); num(size); put(" bytes aligned to "); num(align); + put(", and the arena, of "); num(g_hi - g_lo); put(" bytes, cannot hold them"); + *o = 0; + seal_refused(why); +} + +// [from, to) within what the environment gave, the arena extended to it. +bool hold(kal_u64 from, kal_u64 to) { + from = round_down(from, kGrain); + to = round_up(to, kGrain); + const kal_u64 lo = from < g_lo ? from : g_lo, hi = to > g_hi ? to : g_hi; + if (hi - lo > kMost) return false; + if (from < g_lo) { + if (kal_exec_alloc_at(reinterpret_cast(from), g_lo - from) != reinterpret_cast(from)) return false; + g_lo = from; + } + if (to > g_hi) { + if (kal_exec_alloc_at(reinterpret_cast(g_hi), to - g_hi) != reinterpret_cast(g_hi)) return false; + g_hi = to; + } + return true; +} + +void* take(kal_uintptr size, kal_uintptr align) { + if (align < 16) align = 16; + if (align & (align - 1)) exhausted(size, align); + const kal_u64 n = round_up(size ? size : 1, 16); + if (align < kGrain) { + const kal_u64 at = round_down(g_down - n, align); + if (g_down >= n && at >= g_floor && hold(at, g_down)) { g_down = at; return reinterpret_cast(at); } + } + const kal_u64 at = round_up(g_up, align); + if (!hold(at, at + n)) exhausted(size, align); + g_up = at + n; + return reinterpret_cast(at); +} +void zero(void* p, kal_uintptr n) { + auto* b = static_cast(p); + for (kal_uintptr i = 0; i < n; ++i) b[i] = 0; +} + +OKRUN_IMAGE_ABI void* seal_kal_alloc(kal_uintptr size, kal_uintptr align) { return take(size, align); } +OKRUN_IMAGE_ABI void seal_kal_free(void*, kal_uintptr, kal_uintptr) {} +// A region is reserved, and every part of it backed, at once: the arena's +// memory is the program's. A part whose contents go is zero, as a part +// backed again after them reads. +OKRUN_IMAGE_ABI void* seal_kal_region_reserve(kal_uintptr size, kal_uintptr align, kal_uintptr) { + const kal_uintptr g = kal_memory_granularity(); + return take(round_up(size, g), align > g ? align : g); +} +OKRUN_IMAGE_ABI int seal_kal_region_commit(void*, kal_uintptr) { return kal_ok; } +OKRUN_IMAGE_ABI int seal_kal_region_decommit(void* p, kal_uintptr n) { zero(p, n); return kal_ok; } +OKRUN_IMAGE_ABI int seal_kal_region_discard(void* p, kal_uintptr n) { zero(p, n); return kal_ok; } +OKRUN_IMAGE_ABI void seal_kal_region_release(void*, kal_uintptr) {} +// One context, named by a word no environment gives one: the start that maps +// the snapshot moves what the initialisers recorded of it to its own. +OKRUN_IMAGE_ABI kal_uintptr seal_kal_task_current(void) { return kSealContext; } +OKRUN_IMAGE_ABI void seal_kal_task_yield(void) {} +// No measurement: the start that maps the snapshot is measured, if it asks. +OKRUN_IMAGE_ABI int seal_kal_prof_active(void) { return 0; } + +// FNV-1a, 64 bits. +struct fnv { + kal_u64 h = 14695981039346656037ull; + void bytes(const void* p, kal_uintptr n) { + const auto* b = static_cast(p); + for (kal_uintptr i = 0; i < n; ++i) { h ^= b[i]; h *= 1099511628211ull; } + } + void word(kal_u64 v) { bytes(&v, sizeof v); } +}; + +// THE NAMED VALUES THE INITIALISERS ASKED FOR, one name at a time: answered +// from the environment this runner was started with --- the program's own --- +// and recorded with a word of the answer, which a start compares with its own +// answer for the name (`seal_kept_holds'). Most are absent at every start: a +// library's switch for its own tests, read once into a constant. +struct asked_name { char name[128]; kal_u32 len; kal_intptr value_len; kal_u64 value_word; }; +asked_name g_named[128]; +unsigned g_named_count = 0; +char g_value[65536]; + +// What a start answers for a name: the value's length (negative: absent) and a +// word of its bytes; false where the value is longer than is compared. +bool answer_of(const char* name, kal_uintptr len, kal_intptr& value_len, kal_u64& word) { + value_len = kal_env_var(name, len, g_value, sizeof g_value); + fnv f; + if (value_len > static_cast(sizeof g_value)) return false; + if (value_len > 0) f.bytes(g_value, static_cast(value_len)); + word = f.h; + return true; +} + +OKRUN_IMAGE_ABI kal_intptr seal_kal_env_var(const char* name, kal_uintptr len, char* out, kal_uintptr cap) { + bool known = false; + for (unsigned i = 0; i < g_named_count && !known; ++i) { + const asked_name& a = g_named[i]; + known = a.len == len; + for (kal_uintptr k = 0; known && k < len; ++k) known = a.name[k] == name[k]; + } + if (!known) { + if (len >= sizeof g_named[0].name || g_named_count == sizeof g_named / sizeof g_named[0]) + seal_refused("the initialisers asked for more named values, or a longer name, than a snapshot records"); + asked_name& a = g_named[g_named_count++]; + for (kal_uintptr k = 0; k < len; ++k) a.name[k] = name[k]; + a.len = static_cast(len); + if (!answer_of(name, len, a.value_len, a.value_word)) + seal_refused("an initialiser asked for a named value longer than a snapshot compares"); + } + return kal_env_var(name, len, out, cap); +} + +// THE START'S OWN ANSWERS THAT ARE THE SAME AT MOST STARTS: the processors and +// the cores a program may run on, which a program confined to part of the +// machine is told the part of, and the resource preference it was started +// with. An initialiser that asks for one is answered, the answer is folded into +// the snapshot's facts, and a start that would answer otherwise does not use +// the snapshot: a pool sized when the snapshot was made is sized right. +kal_u64 g_asked = 0; +constexpr kal_u64 kAskedParallelism = 1, kAskedCores = 2, kAskedResources = 4; +OKRUN_IMAGE_ABI kal_uintptr seal_kal_task_parallelism(void) { g_asked |= kAskedParallelism; return kal_task_parallelism(); } +OKRUN_IMAGE_ABI kal_uintptr seal_kal_task_cores(void) { g_asked |= kAskedCores; return kal_task_cores(); } +OKRUN_IMAGE_ABI int seal_kal_env_resources(void) { g_asked |= kAskedResources; return kal_env_resources(); } +OKRUN_IMAGE_ABI kal_u64 seal_kal_interfaces(void) { return own_interfaces(); } + +// A fact of the implementation, answered as at any start. +template struct fact; +template +struct fact { OKRUN_IMAGE_ABI static R call(A... a) { return F(a...); } }; + +// Every other name: the snapshot is refused, naming it. +binding g_names[512]; +unsigned long g_count = 0; +[[noreturn]] void asked(const char* name) { + static char why[160]; + char* o = why; + const char* const parts[] = { "an initialiser asked for ", name, ", which is the start's own" }; + for (const char* t : parts) + while (*t && o < why + sizeof why - 1) *o++ = *t++; + *o = 0; + seal_refused(why); +} +template OKRUN_IMAGE_ABI void refuse() { asked(g_names[I].name); } + +#define OKRUN_BIND(n, i) { #n, reinterpret_cast(&refuse) }, +#define OKRUN_OWN(n, i) OKRUN_BIND(n, i) +#define OKRUN_CALLBACK(n, i) OKRUN_BIND(n, i) +const binding g_main[] = { +#include "surface.inc" +}; +#undef OKRUN_BIND +// openkal.space's names follow the others in `g_names'. +constexpr unsigned long kMain = sizeof g_main / sizeof g_main[0]; +#define OKRUN_BIND(n, i) { #n, reinterpret_cast(&refuse) }, +const binding g_space[] = { +#include "surface_space.inc" +}; +static_assert(kMain + sizeof g_space / sizeof g_space[0] <= sizeof g_names / sizeof g_names[0]); + +const binding g_allowed[] = { + { "kal_alloc", reinterpret_cast(&seal_kal_alloc) }, + { "kal_free", reinterpret_cast(&seal_kal_free) }, + { "kal_region_reserve", reinterpret_cast(&seal_kal_region_reserve) }, + { "kal_region_commit", reinterpret_cast(&seal_kal_region_commit) }, + { "kal_region_decommit", reinterpret_cast(&seal_kal_region_decommit) }, + { "kal_region_discard", reinterpret_cast(&seal_kal_region_discard) }, + { "kal_region_release", reinterpret_cast(&seal_kal_region_release) }, + { "kal_task_current", reinterpret_cast(&seal_kal_task_current) }, + { "kal_task_yield", reinterpret_cast(&seal_kal_task_yield) }, + { "kal_prof_active", reinterpret_cast(&seal_kal_prof_active) }, + { "kal_task_parallelism", reinterpret_cast(&seal_kal_task_parallelism) }, + { "kal_task_cores", reinterpret_cast(&seal_kal_task_cores) }, + { "kal_env_resources", reinterpret_cast(&seal_kal_env_resources) }, + { "kal_env_var", reinterpret_cast(&seal_kal_env_var) }, + { "kal_interfaces", reinterpret_cast(&seal_kal_interfaces) }, + { "kal_version", reinterpret_cast(&fact<&::kal_version>::call) }, + { "kal_memory_granularity", reinterpret_cast(&fact<&::kal_memory_granularity>::call) }, + { "kal_exec_granularity", reinterpret_cast(&fact<&::kal_exec_granularity>::call) }, + { "kal_exec_props", reinterpret_cast(&fact<&::kal_exec_props>::call) }, + { "kal_region_props", reinterpret_cast(&fact<&::kal_region_props>::call) }, + { "kal_time_props", reinterpret_cast(&fact<&::kal_time_props>::call) }, + { "kal_time_monotonic_granularity", reinterpret_cast(&fact<&::kal_time_monotonic_granularity>::call) }, + { "kal_timeout_granularity", reinterpret_cast(&fact<&::kal_timeout_granularity>::call) }, + { "kal_random_props", reinterpret_cast(&fact<&::kal_random_props>::call) }, + { "kal_process_props", reinterpret_cast(&fact<&::kal_process_props>::call) }, + { "kal_task_props", reinterpret_cast(&fact<&::kal_task_props>::call) }, + { "kal_net_props", reinterpret_cast(&fact<&::kal_net_props>::call) }, + { "kal_datagram_props", reinterpret_cast(&fact<&::kal_datagram_props>::call) }, + { "kal_prof_props", reinterpret_cast(&fact<&::kal_prof_props>::call) }, + { "kal_debug_props", reinterpret_cast(&fact<&::kal_debug_props>::call) }, + { "kal_fs_max_name", reinterpret_cast(&fact<&::kal_fs_max_name>::call) }, + { "kal_env_system", reinterpret_cast(&fact<&::kal_env_system>::call) }, +}; + +void fill_names() { + if (g_count) return; + for (const binding& b : g_main) g_names[g_count++] = b; + unsigned long space = 0; + if (space_bindings(space) && space) + for (const binding& b : g_space) g_names[g_count++] = b; + for (const binding& a : g_allowed) + for (unsigned long i = 0; i < g_count; ++i) + if (same(g_names[i].name, a.name)) g_names[i].address = a.address; + // kal_space_props is a fact where the interface is bound. + for (unsigned long i = kMain; i < g_count; ++i) + if (same(g_names[i].name, "kal_space_props")) g_names[i].address = space_props_binding(); +} + +} // namespace + +void seal_begin(kal_u64 image_end) { + fill_names(); + g_floor = round_up(image_end, kGrain); + g_down = g_up = g_lo = g_hi = round_up(g_floor, kPivot); +} + +void seal_arena(kal_u64& lo, kal_u64& hi) { lo = g_lo; hi = g_hi; } + +void* seal_resolve(const char* name) { + fill_names(); + for (unsigned long i = 0; i < g_count; ++i) + if (same(g_names[i].name, name)) return g_names[i].address; + return nullptr; +} + +bool seal_is_binding(kal_uintptr word) { + static kal_uintptr sorted[sizeof g_names / sizeof g_names[0]]; + static unsigned long n = 0; + if (n == 0) { + fill_names(); + for (unsigned long i = 0; i < g_count; ++i) { + kal_uintptr a = reinterpret_cast(g_names[i].address); + unsigned long j = n++; + for (; j > 0 && sorted[j - 1] > a; --j) sorted[j] = sorted[j - 1]; + sorted[j] = a; + } + } + unsigned long a = 0, b = n; + while (a < b) { const unsigned long k = (a + b) / 2; if (sorted[k] < word) a = k + 1; else b = k; } + return a < n && sorted[a] == word; +} + +kal_u64 seal_asked() { return g_asked; } + +// THE NAMED VALUES ASKED FOR, as the bytes kept with the snapshot: for each, a +// kal_u32 length of the name, a kal_u32 zero, the value's length as a kal_i64 +// (negative: absent), a word of the value, and the name, to a multiple of 8. +kal_u64 seal_kept(void* out, kal_u64 cap) { + kal_u64 n = 0; + auto* o = static_cast(out); + auto put = [&](const void* p, kal_u64 k) { + if (o && n + k <= cap) for (kal_u64 i = 0; i < k; ++i) o[n + i] = static_cast(p)[i]; + n += k; + }; + for (unsigned i = 0; i < g_named_count; ++i) { + const asked_name& a = g_named[i]; + const kal_u32 zero = 0; + const kal_u64 value_len = static_cast(static_cast(a.value_len)); + put(&a.len, 4); put(&zero, 4); put(&value_len, 8); put(&a.value_word, 8); + put(a.name, a.len); + const unsigned char pad[8] = {}; + put(pad, (8 - a.len % 8) % 8); + } + return n; +} + +// Whether this start answers every named value the snapshot's initialisers +// asked for as the program that made it answered. +bool seal_kept_holds(const void* kept, kal_u64 size) { + const auto* b = static_cast(kept); + kal_u64 at = 0; + auto get = [&](void* p, kal_u64 k) { + if (at + k > size) return false; + for (kal_u64 i = 0; i < k; ++i) static_cast(p)[i] = b[at + i]; + at += k; + return true; + }; + while (at < size) { + kal_u32 len = 0, zero = 0; + kal_u64 value_len = 0, word = 0; + if (!get(&len, 4) || !get(&zero, 4) || !get(&value_len, 8) || !get(&word, 8) || len >= 128 || at + len > size) + return false; + kal_intptr now_len = 0; + kal_u64 now_word = 0; + if (!answer_of(reinterpret_cast(b + at), len, now_len, now_word)) return false; + if (static_cast(static_cast(now_len)) != value_len || now_word != word) return false; + at += len + (8 - len % 8) % 8; + } + return true; +} + +// WHAT THE IMPLEMENTATION STATES OF ITSELF, as one word: every fact the +// bindings above answer, in one order, and the start's own answers `asked' +// names. A snapshot made under another word --- another implementation, +// another version of it, another machine's granularity, a start confined to +// other processors --- is not used. +kal_u32 environment_facts(kal_u64 asked) { + fnv f; + f.bytes("okrun snapshot 1", 16); + f.word(own_interfaces()); + f.word(kal_version()); + f.word(kal_memory_granularity()); + f.word(kal_exec_granularity()); + f.word(kal_exec_props()); + f.word(kal_region_props()); + f.word(kal_time_props()); + f.word(kal_time_monotonic_granularity()); + f.word(kal_timeout_granularity()); + f.word(kal_random_props()); + f.word(kal_process_props()); + f.word(kal_task_props()); + f.word(kal_net_props()); + f.word(kal_datagram_props()); + f.word(kal_prof_props()); + f.word(kal_debug_props()); + f.word(kal_fs_max_name()); + f.word(space_props()); + f.word(asked); + if (asked & kAskedParallelism) f.word(kal_task_parallelism()); + if (asked & kAskedCores) f.word(kal_task_cores()); + if (asked & kAskedResources) f.word(static_cast(kal_env_resources())); + char system[32]; + const kal_intptr n = kal_env_system(system, sizeof system); + if (n > 0) f.bytes(system, n < static_cast(sizeof system) ? static_cast(n) : sizeof system); + return static_cast(f.h ^ (f.h >> 32)); +} + +} // namespace okrun From eea4e8cf9f14fd2d9eafd0b1b1cdc1156acbb9f8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 07:44:35 +0900 Subject: [PATCH 4/9] wip: README for 0.4.0; the debugger scripts' cache left out Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .gitignore | 1 + README.md | 68 ++++++++++++++---- docs/snapshot.md | 35 ++++++--- .../__pycache__/okgdb_common.cpython-312.pyc | Bin 5465 -> 0 bytes 4 files changed, 81 insertions(+), 23 deletions(-) delete mode 100644 tools/scripts/__pycache__/okgdb_common.cpython-312.pyc diff --git a/.gitignore b/.gitignore index f0e5cca..263d8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target/ compile_commands.json mcpp.lock examples/bind/mathlib/lib/ +__pycache__/ diff --git a/README.md b/README.md index b8ce740..eecf8cd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ architecture that has a runner. |---|---|---| | `openkal-ld` | `libokld` | The format's definition. A reader of bytes, a checker, a signer (Ed25519) and a loader. Freestanding: no C library, no exceptions. Its users are libokrun, `okld`, and a C library's `dlopen`. | | `openkal-run` | `libokrun` | The runner's core. It binds an image's names to this process's one openkal implementation, in the image's calling convention, then enters the image. | -| `openkal-tools` | `okrun`, `okld`, `okprof` | `okrun [--require-signature] [--trust ] [--debugger] [--prof=[:dir]] image\|okb args...` runs an image or an okb. `okld check \| note \| place \| sign \| keygen \| inspect \| note-source \| bundle \| bundle check` works on images and okbs on a build machine. `okprof record \| report \| diff` measures what an image did across openkal. | +| `openkal-tools` | `okrun`, `okld`, `okprof` | `okrun [--require-signature] [--trust ] [--debugger] [--prof=[:dir]] image\|okb args...` runs an image or an okb. `okld check \| note \| place \| touch \| debug-split \| sign \| keygen \| inspect \| note-source \| bundle \| bundle check` works on images and okbs on a build machine. `okprof record \| report \| diff` measures what an image did across openkal. The debugger scripts are in `tools/scripts`. | | `openkal-bind` | `mcpp.mcpplibs.openkal_bind` | A build plugin. A library published as an image (`.okso`) and a C header is called from a program of Linux, macOS or Windows through a binding generated from the header ([docs/bind.md](docs/bind.md)). | `libokld` is the loader at run time and `okld` is the tool on the build machine. They share a @@ -66,14 +66,40 @@ with their output discarded could not start before. **An image at its base** (0.4.0, openkal 0.23): `okld place` chooses a base from the image's bytes (a multiple of 4 GiB from 16 TiB), computes each writable segment for it, every relative relocation applied, and appends the result to the file with a placement note (SPEC 12.4, type 3). -Where the implementation reports `KAL_EXEC_PROP_PLACE`, the loader reserves the image there -(`kal_exec_alloc_at`), takes the stored segments and applies only the relocations that name a +Where the implementation reports `KAL_EXEC_PROP_PLACE`, the loader reserves the image at a base +(`kal_exec_alloc_at`), takes its data for that base and applies only the relocations that name a symbol; each 64 KiB of the read-only-after-relocation data that none of those writes is the -file's pages. Where the base is taken, or the image states none, the loader places it anywhere -and relocates it, as before; `OKRUN_PLACE=0` asks for that. The read-only data of all the -processes of one image are then the same pages, as those of a program linked for one address -are: a start of clangd as an image made 606 page faults at its base, 1 047 anywhere. An image -is placed before it is signed, since the signature covers the stored bytes. +file's pages, and the range is sealed once relocated (`kal_exec_seal_part`, for good for an image +never unloaded). The read-only data of all the processes of one image are then the same pages, as +those of a program linked for one address are: a start of clangd as an image made 606 page faults +at its base, 1 047 anywhere. An image is placed before it is signed, since the signature covers +the stored bytes. + +**A base for each boot.** An image at the base it was sealed with would be at one address on every +machine, which is what address space layout randomisation exists to undo. The runner moves it by +a slide chosen at random once for each user and each boot (a multiple of 64 KiB below 16 TiB), from +the base of its digest; the first start of the boot computes the image's data for that base and +writes it as a prepared copy (`-.okseg`, SPEC 12.4, type 5) in a directory only the +user writes (`OKRUN_CACHE`, else `$XDG_RUNTIME_DIR/okrun`, else the user's cache directory's +`okrun/placed`), and every later start maps it. A new boot is told by the wall clock less the +monotonic one, and removes what was prepared for the last. `OKRUN_PLACE=sealed` keeps the sealed +base, and `OKRUN_PLACE=0` places the image anywhere and relocates it. + +**The pages a start touches** (type 4): `okprof record --what touch` records, through the +implementation (`kal_prof_touch_*`, openkal 0.23), the order in which a start first executes the +pages of an image; `okld touch ...` writes them into the image's touch note, and +`okprof report --order` writes the image's functions in that order for a link to lay them out by. +A loader reads the noted pages in with their part (`KAL_EXEC_MAP_PREFETCH`) where the program's +resource preference spends memory for time: always for performance, never for economy, and where +a quarter of the machine's memory is available when balanced. + +**A snapshot of the initialisers** ([docs/snapshot.md](docs/snapshot.md)): the runner runs an +image's initialisers once in a boot, with memory and the implementation's facts alone, and keeps +the image's data and the memory they obtained as a prepared copy; every later start maps it and +enters the image's C library to take what the start supplies. A start whose facts, processors, +resource preference or named values the initialisers read differ from those the snapshot was +made under does not use it, and an initialiser that asks for anything else of the start is +refused a snapshot. openkal-musl 0.27.0 takes part. `OKRUN_SNAPSHOT=0` runs the initialisers. ## A measurement of an image @@ -93,7 +119,11 @@ beneath the program runs writes `okprof-.json` when it ends: |---|---| | `calls` | each name the image imports, bound through an entry that counts its calls and their time; an image that is not measured is bound as before and pays nothing | | `counters` | the counters and phases the program states through `openkal.prof` (`kal_prof_counter`, `kal_prof_mark`); `kal_prof_active` answers zero when nothing measures | -| `perfmap` | `/tmp/perf-.map` on Linux, where `perf` reads the names of an image's addresses; the runner does not sample, the system's profiler does | +| `perfmap` | `/tmp/perf-.map` on Linux, where `perf` reads the names of an image's addresses | +| `phases` | the moments of a start --- admitted, loaded, initialised, main returned, exit --- with the time and the page faults to each (0.4) | +| `samples[=hz]` | where the processor time goes, sampled by the implementation (`kal_prof_sample_*`, openkal 0.23) and named from the image's symbols; `okprof report --flame` folds the stacks, `--format=trace` lays phases and samples on one timeline for Perfetto (0.4) | +| `touch` | the order of the first execution of each page of an image's instructions (0.4) | +| `names` | `okprof record` grants the program its directories recorded (`KAL_DIR_RECORDED`, openkal 0.23); `okprof report --names` counts, by directory, the names read, written, listed and found absent (0.4) | `openkal.debug` (`kal_debug_symbolize`) names an address from the symbol table of the image that holds it, which the runner reads from the image's file again when it is first asked; the @@ -106,6 +136,16 @@ identity, length and modification time. A later start of the same file admits th shape alone; a file replaced or changed is compared again. A language server that starts a worker per module no longer reads its 146 MB okb at each start. +## A debugger + +An image's instructions and read-only data are its file's pages, so a debugger finds the image +and its base in the process's mappings, and in a core's. `tools/scripts/okgdb.py` (`ok-symbols`, +gdb) and `tools/scripts/oklldb.py` (lldb) read them and load the image's symbols at its base; a +copied image is announced through the GDB JIT interface (`okrun --debugger`). `okld +debug-split` keeps what only a debugger reads apart from the image, as +`.build-id/xx/rest.debug` named by its build-id, where the scripts look for it. A fault the +implementation reports names the frames from the image's symbols (`kal_debug_symbolize`). + ## A library from a program of any system `okrun_open_library`, `okrun_symbol` and `okrun_close` load an image into a program of this @@ -131,9 +171,13 @@ its header and calls it, on openkal and on every other system alike ([docs/bind. - Once an image is loaded, the runner holds no mapping of its own of the image or the okb it came from, only the segments that are the file's pages, and holds its segments once (Linux, from the process's mappings). -- An image at its base, the same image placed anywhere (`OKRUN_PLACE=0`) and the image left - unplaced observe the same memory; placing a placed image changes no byte; a changed byte of - a stored segment is refused by `okld check`, and a signed image is not placed. +- An image at this boot's base, at its sealed base (`OKRUN_PLACE=sealed`), anywhere + (`OKRUN_PLACE=0`) and left unplaced observes the same memory; two starts of a boot choose one + base; placing a placed image changes no byte; a changed byte of a stored segment is refused by + `okld check`, and a signed image is not placed. +- A snapshot is made once, a start uses it, a start whose named value differs and + `OKRUN_SNAPSHOT=0` do not, and an image whose initialiser asks for the time is refused one, + the marker naming the call. - An okb states its images' chunk digests, which okrun verifies on several contexts and `okld bundle check` beside the whole digest; a changed byte is refused by either. - libokld's SHA-256, CRC-32, ZIP reader and writer (with ZIP64) and manifest reader hold diff --git a/docs/snapshot.md b/docs/snapshot.md index e39dc7c..286630c 100644 --- a/docs/snapshot.md +++ b/docs/snapshot.md @@ -37,14 +37,21 @@ The runner making a snapshot loads the image at the base and binds its imports t | --- | --- | | `kal_alloc`, `kal_free`, `kal_region_*` | memory of the arena; nothing is given back | | `kal_memory_granularity`, `kal_exec_granularity`, every `*_props` without an argument, `kal_version`, `kal_interfaces`, `kal_time_monotonic_granularity`, `kal_timeout_granularity`, `kal_fs_max_name`, `kal_env_system` | the implementation's own answer: a fact | +| `kal_task_parallelism`, `kal_task_cores`, `kal_env_resources` | the answer this start gives, recorded: the start's own, and the same at most starts | +| `kal_env_var` | the program's own value of the name, recorded | | `kal_task_current` | one fixed word, the context the initialisers run on | | `kal_task_yield` | nothing to yield to | | `kal_prof_active` | zero | | every other name | refused: the runner ends without a snapshot, and the marker names the call | -The facts are what the implementation states of itself. They are folded into one word, which -the copy records and a start compares with its own: a snapshot made under another -implementation, another version of it or another machine's granularity is not used. +The facts are what the implementation states of itself. They are folded into one word with +the recorded answers --- which of them the initialisers asked for is stated beside it --- and a +start compares the word with its own: a snapshot made under another implementation, another +version of it, another machine's granularity, or for a program confined to other processors, +is not used. A named value an initialiser asked for is recorded by name with a word of its +value (absent is a value), in the runner's bytes kept with the copy, and a start that answers +any of them otherwise does not use the snapshot: a library's switch read once into a constant +(LLVM reads `LLVM_OVERRIDE_PRODUCER` so) holds what this start would have read. **The arena** lies beside a boundary of 256 MiB after the image's span. A region, and any memory aligned to 64 KiB or more, is placed at or above the boundary, aligned from the address itself @@ -70,11 +77,13 @@ void __okrun_snapshot_start(void); ``` **Phase 1.** The runner sets `phase` to 1 before the initialisers run. The C library brings up -its memory and nothing else: no named value, no stream, no randomness, no clock. A named value -read while the phase lasts (`getenv`) is asked of the start, which is refused. After the -initialisers the runner calls `__okrun_snapshot_start`, and the C library reports, by the same -means, what they did that a snapshot cannot hold --- a named value set by one, which the start's -own would replace. +its memory and nothing else: no set of named values, no stream, no randomness, no clock. A named +value read while the phase lasts (`getenv`) is asked of the environment for that one name and +kept, so that a second reading answers the same storage; what an initialiser keeps of it is the +snapshot's memory, and holds the value a start that uses the snapshot has. After the +initialisers the runner calls `__okrun_snapshot_start`, and the C library reports what they did +that a snapshot cannot hold by asking for what the start supplies, which is refused: a named +value set by one is such a thing, since the start's whole set would replace it. **Phase 2.** A start from the snapshot writes the record (`phase` 2, the arena, the context) and calls `__okrun_snapshot_start` before the image's entry. The C library makes the snapshot's @@ -87,9 +96,10 @@ openkal-musl 0.27.0 takes part (`port/src/okm_start.c`). ## The copy -The note's descriptor (`okld_prepared_info`) states the base, the image's digest, the facts, the -arena and its size, the context word, and the offsets from the base of `__okrun_snapshot_start` -and of `__okrun_snapshot`. Its blocks are the 64 KiB blocks of the image's writable segments and +The note's descriptor (`okld_prepared_info`) states the base, the image's digest, the facts and +the answers they fold in, the arena and its size, the context word, the offsets from the base of +`__okrun_snapshot_start` and of `__okrun_snapshot`, and where the runner's own bytes --- the named +values asked for --- lie in the file. Its blocks are the 64 KiB blocks of the image's writable segments and of the arena that hold anything but zeros, joined where at most two blocks of zeros lie between, at file offsets congruent to their addresses modulo 65536; a block lies within one segment or within the arena. @@ -123,5 +133,8 @@ often such addresses, so they are not examined. not take effect; `OKRUN_SNAPSHOT=0` runs the initialisers at the start. - A program whose initialisers read the named values other than through `getenv` (`environ` itself) reads them empty while the snapshot is made, and no refusal reports it. +- The data, and the memory of the snapshot, are read from its file as the program touches them, + and are not read ahead: a start touches little of what its initialisers made (clangd's + `--version`: 311 page faults from the snapshot, 442 without it, and 15 % less processor time). - The allocator's per-heap secrets are drawn once a boot, when the snapshot is made, not once a start. diff --git a/tools/scripts/__pycache__/okgdb_common.cpython-312.pyc b/tools/scripts/__pycache__/okgdb_common.cpython-312.pyc deleted file mode 100644 index 601a7b5df76e998f57a065a7fffee7ec66b6eda5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5465 zcmbVQTTmO<89poRu5JiP$bfIe>p+6#*eEs^+a#go?pttd$BE52A&Yhq0-;rQ7Xiv@ zOgnuLF)4W387nh+P?|g_b*DpSIt^(bYNvhZOkcFf3CfnY=7pI)5ZtM!ZfDZ}tZpEa z@w7*yv*-Sw%YVNA`_EtOb}NEFk?Uh$RUq^?oGAoDKJnlrByJ-PaYPUe=Dx%r!5Myn z1`V7MVk1XFOmY;&6h}i$b0&yQGla*yj)xiW5&6;Z#{z#N{2%-sN^c{%Sf_xDJ9)XS zC`Ks67;aYmj>dV>=0`r<0)B;Y33M6BRAI<5 zx{xY;ITMUxzLS@H9c0CfPv~KR9@cF=NSHfbXIW5;g}D>xdsb&v$SBOO&@M)m&)<(? zv}`L3e1j~s>_Kn*0Oo3X!dz`PD9);wbQy&;nxUULW{Fv2wpc~X9;=Kw6f;*behBuI zm_MS>WA=i)(pRx6HpM=&WkvIyBvRz9p#yz zz;f(xkY^;`Kf#A&i4jH^HpKLv?_)*+LB5?iB#ewmynKX-upB2I(MzSMr%5x2!|OkU zISj0m5I?}t4d}LxwdRrG#f$4qE^&GttL-Dpi)YSU)WbT~8IMULg)kq|NRekbjh19_ z(kE-S$xxW}jSr28!h}XjJU^~cA{+Aa8qJ2GIp=X|WSEslHDW{~M{!pwJPK39pU~ul zM#*?Nno*dPH6j3|@=O>y4~+y_zogN*#b}hi7$^?oMQJo%suYF%1V5pF*m3y)c&`-Z*>UfbAdX8gW8ORA3x|V&VabbEHO>Z)1}0cP@0A51D0wBH7zoQy zH}2<#hkU}sgb->9&uHetwu_A*$^?JO0=qMdzOmTj++t70vS$OCsQSdkti!nwnTsqA z-;Bn|tj)1#SaiitE=E^u2a~U+4u1Ih67h@cS$FkA6Ey!8_xXnzRqbAN)vK=hWKXJL z$&hh1&(U#e%~iWYY1V96prLwl-kx>3Zqv8uMKRHPE1GdW9j6{TV7v_{ve{?5*X&h` ztv9c4A|v&DoLK9BdA0wV+J7xQ$fy0mbSRvbWHlsb`X^O$Pn?QJ79)wtlile**jL}ETX|7#M>7#;(~f965a z5j}-Lv47wSGdvjxaz_JP86F;C0wHFUpJqAU2Nyhegb_q$KnU?oA_l+#5g(|CO}U7t zE`ljXB1UWxo40kGjgo+bXVx*`um9(}@4j1iKuW?{kn5pEoE0(hh*Dje;CNHc<5 zk6Wz41+}<<sdrI>#W2KG&%(S$xw)CkjeRtnUzj`%2IFugd(*99(cvKx6 z&9nqm^D)30aj`9NJRv6AQpZ!Gjy3NB*6hmK-El*<(z$qi&Y!4BKKuTGOy!}qhQq53 zZE8c?Qe;`adnwa!TCF%3?_2Ck^a6?|47W}sA}QkiXzG&slvi~>AMeRJsuQkv-$}^H zu2j{B{i&{{s!#gU2U?ctl>;4jTmRhoMSHe(U!o_uD@A`;m#+1$);6oP&6(QP6=!SK zRh@V?dF=heDTBJ_u63g!Sk>SC8BH zCpXoze>?bLrNWx?U>Ty@K#-c_t*t zP;%(3{O@mURgj{pzjo}}fGbh`NWy}CT+wMq zKlOAH#?LH}x<^o*R?|J&h|^X*wp%*)81L;SAl}*f{T8m^cI#I-DooxbgM!I+p3oyS zRy+=no)2Sq8_R-HB)@)QOK-f_Wnm|z=D;;6sK-vUb<2pAWy3+XF#jI3VDK5R`|t+L z5IY(`glWZa%P?;tp)_XDEsaqz0_}bUx7af7PV2<5S{A@E8x?{B$7wG(101D0GBh6d zPJViG&CN3aiPO@O|#u!+bXjTcigsC>0I#7`4@yaA;D!T>(lmz-yHv}{nPf(IzH`4cU?|j zc|Fs7b+!3TwfW8aL+t8MKphHXn#aCs45!$Yknl@lr7^79!?S&03NUf41B6Lx#J;TvikGh=^(Z8EzNnzS4G3L_ zRxx9Yj8TeF0mYv-U~(o}AV*<^9EVE`>QoZ~1)ATZ5DJNjU=%0^uEpPint{)6h@Qo& zr7KM%s0%Cc;K6}!#js466=h}udsQ66%FJ65a0yU1y;j%7oUa5bsXKs;O#>c6cT<3) zU=9KrfqUe5h7}x{p;%7_tBf1 z$UwcInh(aUi@k}qtgSjxbKlmQJe-QCPq%($Yt6cMCwh~wrN-1}POAG)sqXfbiuMf( zI&IK!5)WW3v~AO|p6i?bmX3}F1Y1Rv9yVft8KP~A$t*h15;0?taZf$KV@UDQ0v=-`6m}QG+V1YU_KHQSO0lnc- zC1$-rf$MKAJwcQnHE?^tGTo?yy6lE_^!Rl+7`u$=s2&u+I;Hqn60$hyx2U$YNxOZyV7*0(KFnVJr`UX~dm-UkwZ=C*y@ouWQE ztL|$`JG@J+8OO=&iI?u~T{&^?=TpFOucZ3!*EXjwzJ|%RZz3IzsJ@6gIHg{kzVDp= zmcm1AP+0zk6%$s+cEb9xtE<8IMFZ7!)Z{UXov`4XH`9ny*l*BDmX7@TzU!Ow2@)^i zD(q%FGQT(>aFap4LmY%G++QR;2NAF)f_Ok9!unTa-=rOcW3$0UI5!W`MD=Es6;gXC N!uh~yC!G3V{{<6J)FS`@ From 9a202487de5f2ca4c31e5a330e9c4a28fca77f22 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 07:45:03 +0900 Subject: [PATCH 5/9] wip: okprof and the debugger's scripts released beside okrun and okld Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/release-binaries.yml | 11 ++++++++--- tools/src/okprof.cpp | 2 +- tools/src/okrun.cpp | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 5f5b8a0..d9ca9fa 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -101,23 +101,28 @@ jobs: echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" fi - - name: okrun and okld, packed + - name: okrun, okld and okprof, packed working-directory: tools run: | tag='${{ github.event.inputs.tag }}' mkdir -p ../out - for tool in okrun okld; do + for tool in okrun okld okprof; do mcpp pack "$tool" ${{ matrix.pack }} --platform "openkal-platform@$PLATFORM_VERSION" \ --pkg-format zip -o "$tool-$tag-${{ matrix.id }}.zip" cp "target/dist/$tool-$tag-${{ matrix.id }}.zip" ../out/ done + # The debugger's scripts (okgdb.py, oklldb.py) are the same on every + # system: one archive, from one row. + if [ '${{ matrix.id }}' = linux-x86_64 ]; then + (cd scripts && zip -q -X "../../out/openkal-debugger-scripts-$tag.zip" okgdb.py oklldb.py okgdb_common.py) + fi ls -l ../out - name: Each program runs here, from its package alone run: | python=python3; [ "$RUNNER_OS" = Windows ] && python=python tag='${{ github.event.inputs.tag }}' - for tool in okrun okld; do + for tool in okrun okld okprof; do root="$RUNNER_TEMP/x-$tool" # Unpacked as an unzip program does it, each file with the mode the # archive states (a program that lost its x bit fails here); the diff --git a/tools/src/okprof.cpp b/tools/src/okprof.cpp index a1cb6c8..fa9e3ae 100644 --- a/tools/src/okprof.cpp +++ b/tools/src/okprof.cpp @@ -661,7 +661,7 @@ int record_cmd(std::vector args) { int main(int argc, char** argv) { std::vector args(argv + 1, argv + argc); if (args.empty()) { - std::println(std::cerr, "okprof record|report|diff ... (see the head of tools/src/okprof.cpp)"); + std::println(std::cerr, "usage: okprof record|report|diff ... (see the head of tools/src/okprof.cpp)"); return 2; } const std::string command = args.front(); diff --git a/tools/src/okrun.cpp b/tools/src/okrun.cpp index 43e5a23..b5c887f 100644 --- a/tools/src/okrun.cpp +++ b/tools/src/okrun.cpp @@ -47,7 +47,7 @@ int main(int argc, char** argv) { } options.trusted = keys; if (i >= argc) { - say("usage: okrun [--require-signature] [--trust ]... [--debugger] image|okb [arguments...]\n"); + say("usage: okrun [--require-signature] [--trust ]... [--debugger] [--prof=[:dir]] image|okb [arguments...]\n"); return 2; } char why[512]; From 7e52170b6b6f1fffe22072e4a44eccdd95ee443e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 07:52:32 +0900 Subject: [PATCH 6/9] wip: a start's phases name its snapshot Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 2 +- run/src/prof.cpp | 6 +++--- run/src/run.cpp | 4 +++- run/src/runner.h | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eecf8cd..8643f9f 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ beneath the program runs writes `okprof-.json` when it ends: | `calls` | each name the image imports, bound through an entry that counts its calls and their time; an image that is not measured is bound as before and pays nothing | | `counters` | the counters and phases the program states through `openkal.prof` (`kal_prof_counter`, `kal_prof_mark`); `kal_prof_active` answers zero when nothing measures | | `perfmap` | `/tmp/perf-.map` on Linux, where `perf` reads the names of an image's addresses | -| `phases` | the moments of a start --- admitted, loaded, initialised, main returned, exit --- with the time and the page faults to each (0.4) | +| `phases` | the moments of a start --- admitted, loaded, its snapshot entered, initialised, main returned, exit --- with the time and the page faults to each (0.4) | | `samples[=hz]` | where the processor time goes, sampled by the implementation (`kal_prof_sample_*`, openkal 0.23) and named from the image's symbols; `okprof report --flame` folds the stacks, `--format=trace` lays phases and samples on one timeline for Perfetto (0.4) | | `touch` | the order of the first execution of each page of an image's instructions (0.4) | | `names` | `okprof record` grants the program its directories recorded (`KAL_DIR_RECORDED`, openkal 0.23); `okprof report --names` counts, by directory, the names read, written, listed and found absent (0.4) | diff --git a/run/src/prof.cpp b/run/src/prof.cpp index 413394e..4b2f898 100644 --- a/run/src/prof.cpp +++ b/run/src/prof.cpp @@ -16,9 +16,9 @@ // counters the counters and phases the program registers are kept // perfmap /tmp/perf-.map is written, where this system's profiler // (perf) reads the names of an anonymous region's addresses -// phases the moments of the start --- admitted, loaded, initialised, -// main returned --- with the time and the page faults to each -// (0.4) +// phases the moments of the start --- admitted, loaded, its snapshot +// entered where it starts from one, initialised, main returned --- +// with the time and the page faults to each (0.4) // samples[=hz] where the program's processor time goes, sampled by the // implementation (openkal 0.23) at hz a second (1000), each // stack named from the images' symbol tables (0.4) diff --git a/run/src/run.cpp b/run/src/run.cpp index 5c11727..1016632 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -1393,7 +1393,9 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam start_making(lp); profile_begin_run(); if (from_snapshot) { - // THE SECOND PHASE: the C library takes what this start supplies. + // THE SECOND PHASE: the C library takes what this start supplies. A + // measurement of the start's phases names it. + profile_phase("snapshot"); auto* base = static_cast(okld_base(m)); *reinterpret_cast(base + snap.state) = snapshot_record{ snap.arena, snap.arena_size, 2, 0, snap.context }; call_image(base + snap.start, nullptr); diff --git a/run/src/runner.h b/run/src/runner.h index 3c494b5..76d3bdf 100644 --- a/run/src/runner.h +++ b/run/src/runner.h @@ -31,8 +31,8 @@ bool profile_calls(); bool profile_samples(); // `samples': this runner takes them (0.4) bool profile_touches(); // `touch': this runner records them (0.4) // The phases of a start (0.4, `phases'): each mark is a moment of the start --- -// admitted, loaded, relocated, initialised, main returned --- with the time and -// the page faults so far. +// admitted, loaded, a snapshot mapped, initialised, main returned --- with the +// time and the page faults so far. void profile_phase(const char* name); // What a measurement takes once the image is loaded and before it runs: the // samples begun, the touches of its executable parts begun (`okld_iterate'). From 065086c1be58366e9b09d87095d134d72acbef00 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 08:37:55 +0900 Subject: [PATCH 7/9] wip: what a start spends by its resource preference (prefetch levels, the placement's data in memory for economy, the snapshots' bound), okrun cache, a compressed okb (okb 2) expanded into the cache, the runner's choices in a report Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 49 ++++++ README.md | 24 +++ docs/okb.md | 23 ++- ld/include/okld.h | 29 +++- ld/src/inflate.cpp | 177 +++++++++++++++++++++ ld/src/load.cpp | 6 +- ld/src/okb.cpp | 27 +++- ld/src/okld.cppm | 2 + ld/src/zip.cpp | 24 +-- ld/tests/okb_test.cpp | 90 +++++++++++ run/include/okrun.h | 13 ++ run/src/okrun.cppm | 2 + run/src/prof.cpp | 23 +++ run/src/run.cpp | 331 ++++++++++++++++++++++++++++++++++++--- run/src/runner.h | 3 + tools/mcpp.toml | 5 + tools/src/okld.cpp | 94 ++++++++--- tools/src/okrun.cpp | 36 +++++ 18 files changed, 889 insertions(+), 69 deletions(-) create mode 100644 ld/src/inflate.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 950340a..5e10b19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,34 @@ jobs: "$OKLD" bundle out/resident.okb out/x86_64/resident.oke out/aarch64/resident.oke "$OKLD" bundle out/needed.okb --entry needed.oke \ out/x86_64/needed.oke out/x86_64/liboktwice.okso out/aarch64/needed.oke out/aarch64/liboktwice.okso + # COMPRESSED (okb 2, 0.4.0): the economy form, its images DEFLATE. + "$OKLD" bundle out/needed-z.okb --compress --entry needed.oke \ + out/x86_64/needed.oke out/x86_64/liboktwice.okso out/aarch64/needed.oke out/aarch64/liboktwice.okso + "$OKLD" bundle check out/needed-z.okb + python3 - out/needed-z.okb <<'PY' + import json, sys, zipfile + z = zipfile.ZipFile(sys.argv[1]) + assert z.testzip() is None + for i in z.infolist(): + want = 8 if i.filename.endswith(('.oke', '.okso')) else 0 + assert i.compress_type == want, (i.filename, i.compress_type) + assert json.loads(z.read('okb.json'))['okb'] == 2 + print(' ok a compressed okb: its images DEFLATE, its manifest stored, version 2') + PY + python3 - out/needed-z.okb out/tampered-z.okb <<'PY' + import sys, zipfile + d = bytearray(open(sys.argv[1], 'rb').read()) + i = zipfile.ZipFile(sys.argv[1]).getinfo('x86_64/liboktwice.okso') + import struct + n, e = struct.unpack(' tampered-z.txt; then echo "::error::a changed compressed okb passed bundle check"; exit 1; fi + cat tampered-z.txt "$OKLD" bundle out/signed.okb --sign "$RUNNER_TEMP/seed" --entry needed.oke \ out/x86_64/needed.oke out/x86_64/liboktwice.okso out/aarch64/needed.oke out/aarch64/liboktwice.okso # From a universal package mcpp staged (mcpp-package.json, SPEC-014 §4.5). @@ -307,6 +335,17 @@ jobs: out=$(OKRUN_SNAPSHOT=0 run snapshot); expect "$out" 'started by its initialisers' 'OKRUN_SNAPSHOT=0 used the snapshot' out=$(run snapshot-timed); expect "$out" 'started by its initialisers' 'an image refused a snapshot started from one' echo " ok a snapshot is made once, used, compared, declined and refused" + # WHAT THE RUNNER KEEPS (0.4.0): listed, and an economy start writes + # nothing to it. + "$OKRUN" cache | tee cache.txt + grep -q '\.oksnap' cache.txt || { echo "::error::okrun cache lists no snapshot"; exit 1; } + export OKRUN_CACHE="$RUNNER_TEMP/okcache-economy" + mkdir -p "$OKRUN_CACHE" + KAL_RESOURCES=economy "$OKRUN" "out/$a/snapshot.oke" > /dev/null + sleep 1 + if ls "$OKRUN_CACHE" | grep -qv '^boot$'; then echo "::error::an economy start wrote to the cache"; ls "$OKRUN_CACHE"; exit 1; fi + "$OKRUN" cache gc --all + echo " ok okrun cache lists what it keeps; an economy start writes nothing" - name: The refusals run: | a='${{ matrix.arch }}' @@ -344,6 +383,16 @@ jobs: OKRUN_PATH="nowhere${sep}${here}/path" "$OKRUN" alone.okb | tr -d '\r' | grep -q "twice(21) = 42" \ || { echo "::error::OKRUN_PATH"; exit 1; } echo " ok the okb runs; an unsigned or changed okb is refused; OKRUN_PATH is searched" + # COMPRESSED (0.4.0): expanded once into the user's cache, then mapped + # from there; a changed byte of it is refused. + export XDG_CACHE_HOME="$RUNNER_TEMP/xdg" + for i in 1 2; do + z=$("$OKRUN" out/needed-z.okb | tr -d '\r') + grep -q "twice(21) = 42, next_value() = 41" <<<"$z" || { echo "::error::a compressed okb, start $i"; echo "$z"; exit 1; } + done + if "$OKRUN" out/tampered-z.okb 2> e7.txt; then echo "::error::a changed compressed okb ran"; exit 1; fi + cat e7.txt + echo " ok a compressed okb runs, expanded once; a changed one is refused" - name: What stays resident once an image is loaded (Linux) if: runner.os == 'Linux' run: | diff --git a/README.md b/README.md index 8643f9f..0aa6a9e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,27 @@ resource preference or named values the initialisers read differ from those the made under does not use it, and an initialiser that asks for anything else of the start is refused a snapshot. openkal-musl 0.27.0 takes part. `OKRUN_SNAPSHOT=0` runs the initialisers. +## What a start spends + +A program's resource preference (openkal 0.23: balanced, performance, economy) decides what +the runner spends ahead of it; balanced is taken as economy where memory available falls below +15 % of the machine's, or the cache's volume below 1 GiB free (mcpp design 2026-11-03 part 4 §16): + +| point | performance | balanced | economy | single setting | +| --- | --- | --- | --- | --- | +| the data for this boot's base | kept in the cache | kept in the cache | computed for each start, written nowhere | `OKRUN_PLACE` | +| a snapshot of the initialisers | made; the snapshots bound at 2 GiB | made; bound at 512 MiB, the least recently used removed | used where one exists, not made | `OKRUN_SNAPSHOT` | +| what is read ahead | the touched pages and the data | the touched pages | nothing | `OKRUN_PREFETCH=0\|touch\|all` | + +A compressed okb (okb 2, `okld bundle --compress`; `mcpp pack --pkg-format okb --compress gzip`) +is the economy form of a package: kept small on disk, its images expanded into the user's cache +(`okrun/expanded`) when it first runs and mapped from there afterwards. + +`okrun cache` lists what the runner keeps for this machine, `okrun cache gc` removes what was +prepared for another boot, and `okrun cache gc --all` everything, remembered admissions, +digests and expanded images included. A measurement's report states what the runner chose at +each point (`runner`: the placement's data, the snapshot, what was read ahead, an expansion). + ## A measurement of an image The runner binds every name an image calls across openkal and read the image's file, so it @@ -178,6 +199,9 @@ its header and calls it, on openkal and on every other system alike ([docs/bind. - A snapshot is made once, a start uses it, a start whose named value differs and `OKRUN_SNAPSHOT=0` do not, and an image whose initialiser asks for the time is refused one, the marker naming the call. +- A compressed okb is checked by `okld bundle check`, runs on all four machines, expanding its + image once into the cache, and a changed byte of it is refused; an economy start writes + nothing to the runner's cache; `okrun cache` lists and removes what it keeps. - An okb states its images' chunk digests, which okrun verifies on several contexts and `okld bundle check` beside the whole digest; a changed byte is refused by either. - libokld's SHA-256, CRC-32, ZIP reader and writer (with ZIP64) and manifest reader hold diff --git a/docs/okb.md b/docs/okb.md index d2f7d01..9c305bf 100644 --- a/docs/okb.md +++ b/docs/okb.md @@ -17,7 +17,7 @@ them. | Rule | Statement | |---|---| -| C1 | Every member is **stored** (method 0): its bytes in the archive are its content. | +| C1 | Every member is **stored** (method 0): its bytes in the archive are its content --- except, in an okb of version 2, an image member, which may be **compressed** (DEFLATE, method 8; §5). | | C2 | The bytes of every image member (a name ending in `.oke` or `.okso`) begin at an offset that is a multiple of **65536** from the start of the archive. 65536 is the image's own segment alignment (`-z max-page-size=65536`, SPEC 12.2), so an image's segments can be mapped where they lie. | | C3 | No member is encrypted. | | C4 | A reader finds members through the central directory only. Bytes that no directory entry names may lie between members. | @@ -98,8 +98,11 @@ A reader that runs an okb: 3. selects the architecture of the machine it runs on, and refuses an okb that holds none for it, naming the architectures it holds; 4. admits each member it uses: stored (C1), an image's bytes aligned (C2), and its SHA-256 the - one `files` states --- or, where `files` states its chunks, each chunk's (§3). A member that does not hold is refused; a reader does not decompress, - realign or copy a member to make it hold; + one `files` states --- or, where `files` states its chunks, each chunk's (§3). A member that does not hold is refused; a reader does not + realign or copy a stored member to make it hold. A compressed image (okb 2) is expanded once into + a file of the reader's own --- okrun's is the user's cache, `okrun/expanded`, named by the + manifest's digest and the member --- held to the SHA-256 `files` states, and mapped from there; + a later start of the same okb file, whose admission is remembered, maps it again; 5. resolves the entry and each image an image needs (DT_NEEDED) in this order: 1. the architecture's directory, then the archive's root; 2. beside the okb, then its `lib//` and `lib/`; @@ -120,8 +123,8 @@ has no such interface in version 0.17. ## 6. Writing ``` -okld bundle [--name ] [--version ] [--entry ] [--sign ] ... -okld bundle --package [--sign ] +okld bundle [--name ] [--version ] [--entry ] [--sign ] [--compress] ... +okld bundle --package [--sign ] [--compress] okld bundle check [--require-signature] [--trust ]... ``` @@ -130,6 +133,12 @@ architecture's directory, and writes `okb.json` with the digests; `--sign` signs and the manifest with one key. The archive is deterministic: no time is recorded, and the same images give the same bytes. The entry defaults to the first `.oke` named. +`--compress` stores each image compressed (DEFLATE) and writes an okb of version 2, without the +images' chunk digests: an okb kept small on disk, which a runner expands into its cache when it +first runs it --- the economy form of a package (openkal-platform's plugin writes it where the +package states `[package.metadata.okb] form = "compressed"`). A stored okb is written as version 1, +which every reader reads. + `--package` takes the images from a universal package that mcpp has staged (mcpp SPEC-014 §4.2, §4.5), reading `mcpp-package.json` at the root of the tree: @@ -143,7 +152,7 @@ Files of every other role (launchers, loaders, resources, metadata) are not memb version 1 okb. This is the action of the `okb` package format that openkal-platform's plugin provides. -`okld bundle check` verifies C1 to C3, the manifest, every digest, every signature (and the +`okld bundle check` verifies C1 to C3 (a compressed image expanded and held to its digest), the manifest, every digest, every signature (and the trust asked for), that each architecture's entry is present, that each image is of its directory's architecture, and that each image follows the convention (SPEC 12.2) with the exports of the other images of its directory. It prints one line per violation. @@ -154,6 +163,7 @@ exports of the other images of its directory. It prints one line per violation. |---|---|---| | `okld_zip_each`, `okld_zip_find`, `okld_zip_write` | libokld | The ZIP container, read from memory and written into a given buffer. | | `okld_okb_read_manifest`, `okld_okb_digest`, `okld_okb_each_file`, `okld_okb_admit` | libokld | The manifest and the admission of a member (§5, step 4). | +| `okld_okb_expand`, `okld_inflate` | libokld | A compressed member expanded and held to its digest; DEFLATE read (okb 2). | | `okrun_run_file`, `okrun_run_bytes` | libokrun | An image or an okb, from a file or from memory. | | `okrun_run_okb_bytes` | libokrun | An okb, or an archive of images, from memory, with the entry named or taken from `okb.json`. | | `okrun_run_images` | libokrun | A group of images in memory that resolve one another by name. | @@ -165,3 +175,4 @@ exports of the other images of its directory. It prints one line per violation. | 1 | 0.1.2 | This document. | | 1 | 0.1.3 | `okld bundle --package`: an okb from a package mcpp staged. | | 1 | 0.1.8 | `files..chunks`: an image's chunk digests, written by `okld bundle`, verified by okrun on several contexts at once and by `okld bundle check` beside the whole digest. Still okb 1: a reader that does not know the key ignores it. | +| 2 | 0.4.0 | An image member may be compressed (DEFLATE), `okld bundle --compress`; okrun expands it once into its cache and maps it there. An okb whose members are all stored is still written as version 1. | diff --git a/ld/include/okld.h b/ld/include/okld.h index af077dd..3e15eff 100644 --- a/ld/include/okld.h +++ b/ld/include/okld.h @@ -121,7 +121,7 @@ okld_u32 okld_crc32(okld_u32 crc, const void* bytes, okld_size size); /* --- ZIP archives and okb v1 (docs/okb.md) -------------------------------- */ -#define OKLD_OKB_VERSION 1u +#define OKLD_OKB_VERSION 2u /* 2: an image member may be compressed (0.4) */ #define OKLD_OKB_ALIGNMENT 65536u /* A member of a ZIP archive that lies in memory. `name' points into the @@ -150,12 +150,17 @@ int okld_zip_find(const void* archive, okld_u64 size, const char* name, struct okld_zip_member* out, struct okld_why* why); /* A member to write: stored; when `align' is not zero its bytes begin at a - * multiple of the archive's alignment. */ + * multiple of the archive's alignment. With `method' 8 (version 0.4), `bytes' + * are a DEFLATE stream of `size' bytes that expands to `original' bytes whose + * CRC-32 is `crc32'. */ struct okld_zip_input { const char* name; const void* bytes; okld_u64 size; int align; + okld_u32 method; + okld_u64 original; + okld_u32 crc32; }; /* Writes a ZIP archive of the members, in order, into `out' and returns its * size; when `out' is null or `cap' is smaller, writes nothing and returns @@ -201,6 +206,17 @@ int okld_okb_is_image(const char* name, okld_size name_len); * filled. Nothing is copied. */ int okld_okb_admit(const void* archive, okld_u64 size, const struct okld_zip_member* member, const void* json, okld_size json_size, struct okld_why* why); +/* A MEMBER STORED COMPRESSED (DEFLATE, method 8; docs/okb.md, version 0.4), + * expanded into `out' (at least its original size) and held to the content + * `json' states (its SHA-256), or without a manifest to its CRC-32. A runner + * expands it once, into a file of its own, and maps that. 0, or negative with + * `why' filled. */ +int okld_okb_expand(const void* archive, okld_u64 size, const struct okld_zip_member* member, + const void* json, okld_size json_size, void* out, okld_u64 cap, struct okld_why* why); +/* DEFLATE, read (RFC 1951): what `in' expands to, into `out', at most `cap' + * bytes; `*ok' is 1 when the stream was whole and well formed. Returns the + * bytes written. Version 0.4. */ +okld_u64 okld_inflate(const void* in, okld_u64 in_size, void* out, okld_u64 cap, int* ok); /* A member admitted by its shape alone: stored, within the archive, and an * image's bytes at a multiple of OKLD_OKB_ALIGNMENT. Its content is not read: * for a member whose content was compared before, on the same file (okrun's @@ -285,10 +301,11 @@ struct okld_host { * runner's entry image and those it needs --- so their read-only-after- * relocation range is sealed for good. Version 0.4. */ int permanent; - /* Non-zero: the pages an image states it touches when it starts (SPEC - * 12.4, type 4) are read in as their part is mapped (OKLD_MAP_PREFETCH); - * zero spends no memory ahead of the program. A runner sets it by the - * program's resource preference. Version 0.4. */ + /* What is read in as its part is mapped (OKLD_MAP_PREFETCH): 1, the pages + * an image states it touches when it starts (SPEC 12.4, type 4); 2, those + * and the data of an image at a base (its stored segments or a prepared + * copy's, not a snapshot's); 0 spends no memory ahead of the program. A + * runner sets it by the program's resource preference. Version 0.4. */ int prefetch; }; diff --git a/ld/src/inflate.cpp b/ld/src/inflate.cpp new file mode 100644 index 0000000..200f63b --- /dev/null +++ b/ld/src/inflate.cpp @@ -0,0 +1,177 @@ +// DEFLATE, read (RFC 1951): the bytes of an okb member stored compressed +// (ZIP method 8), which a runner expands once before it maps the image +// (docs/okb.md, 0.4). Freestanding, as the rest of this library is: the caller +// gives the output and nothing is allocated. +#include + +namespace { + +using u8 = unsigned char; using u16 = unsigned short; using u32 = unsigned int; using u64 = unsigned long long; + +struct reader { + const u8* in; u64 n, at = 0; + u32 bits = 0; int count = 0; + bool failed = false; + // `need' bits, least significant first. + u32 take(int need) { + u64 v = bits; + while (count < need) { + if (at == n) { failed = true; return 0; } + v |= u64(in[at++]) << count; + count += 8; + } + bits = u32(v >> need); + count -= need; + return u32(v & ((u64(1) << need) - 1)); + } +}; + +// A canonical Huffman code: how many codes of each length, and the symbols in +// the order of their codes. +struct code { + u16 counts[16]; + u16 symbols[288]; +}; + +bool build(code& c, const u8* lengths, int n) { + for (int i = 0; i < 16; ++i) c.counts[i] = 0; + for (int i = 0; i < n; ++i) c.counts[lengths[i]]++; + if (c.counts[0] == n) return true; // no code at all: legal for distances + int left = 1; + for (int len = 1; len < 16; ++len) { + left <<= 1; + left -= c.counts[len]; + if (left < 0) return false; // more codes than the lengths allow + } + u16 offs[16]; + offs[1] = 0; + for (int len = 1; len < 15; ++len) offs[len + 1] = u16(offs[len] + c.counts[len]); + for (int i = 0; i < n; ++i) + if (lengths[i] != 0) c.symbols[offs[lengths[i]]++] = u16(i); + return true; +} + +int decode(reader& r, const code& c) { + int value = 0, first = 0, index = 0; + for (int len = 1; len < 16; ++len) { + value |= int(r.take(1)); + if (r.failed) return -1; + const int count = c.counts[len]; + if (value - count < first) return c.symbols[index + (value - first)]; + index += count; + first += count; + first <<= 1; + value <<= 1; + } + return -1; +} + +constexpr u16 kLengthBase[29] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 }; +constexpr u8 kLengthExtra[29] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; +constexpr u16 kDistBase[30] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 }; +constexpr u8 kDistExtra[30] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 }; + +// One block's symbols, through its two codes, into `out'. +bool codes(reader& r, const code& lit, const code& dist, u8* out, u64 cap, u64& at) { + for (;;) { + const int sym = decode(r, lit); + if (sym < 0) return false; + if (sym < 256) { + if (at == cap) return false; + out[at++] = u8(sym); + continue; + } + if (sym == 256) return true; + const int li = sym - 257; + if (li >= 29) return false; + const u64 len = kLengthBase[li] + r.take(kLengthExtra[li]); + const int di = decode(r, dist); + if (di < 0 || di >= 30) return false; + const u64 d = kDistBase[di] + r.take(kDistExtra[di]); + if (r.failed || d > at || len > cap - at) return false; + for (u64 k = 0; k < len; ++k, ++at) out[at] = out[at - d]; + } +} + +bool fixed(reader& r, u8* out, u64 cap, u64& at) { + code lit, dist; + u8 l[288]; + for (int i = 0; i < 144; ++i) l[i] = 8; + for (int i = 144; i < 256; ++i) l[i] = 9; + for (int i = 256; i < 280; ++i) l[i] = 7; + for (int i = 280; i < 288; ++i) l[i] = 8; + build(lit, l, 288); + for (int i = 0; i < 30; ++i) l[i] = 5; + build(dist, l, 30); + return codes(r, lit, dist, out, cap, at); +} + +bool dynamic(reader& r, u8* out, u64 cap, u64& at) { + const int nlen = int(r.take(5)) + 257, ndist = int(r.take(5)) + 1, ncode = int(r.take(4)) + 4; + if (r.failed || nlen > 286 || ndist > 30) return false; + static constexpr u8 kOrder[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + u8 lengths[320] = {}; + for (int i = 0; i < ncode; ++i) lengths[kOrder[i]] = u8(r.take(3)); + code lencode; + if (r.failed || !build(lencode, lengths, 19)) return false; + for (int i = 0; i < 19; ++i) lengths[i] = 0; + int index = 0; + while (index < nlen + ndist) { + int sym = decode(r, lencode); + if (sym < 0) return false; + if (sym < 16) { lengths[index++] = u8(sym); continue; } + u8 len = 0; + int repeat; + if (sym == 16) { + if (index == 0) return false; + len = lengths[index - 1]; + repeat = 3 + int(r.take(2)); + } else if (sym == 17) { + repeat = 3 + int(r.take(3)); + } else { + repeat = 11 + int(r.take(7)); + } + if (r.failed || index + repeat > nlen + ndist) return false; + while (repeat--) lengths[index++] = len; + } + if (lengths[256] == 0) return false; // a block that cannot end + code lit, dist; + if (!build(lit, lengths, nlen) || !build(dist, lengths + nlen, ndist)) return false; + return codes(r, lit, dist, out, cap, at); +} + +} // namespace + +extern "C" okld_u64 okld_inflate(const void* in, okld_u64 in_size, void* out, okld_u64 cap, int* ok) { + reader r{ static_cast(in), in_size }; + u8* o = static_cast(out); + u64 at = 0; + bool last = false, good = true; + while (!last && good) { + last = r.take(1) != 0; + const u32 type = r.take(2); + if (r.failed) { good = false; break; } + if (type == 0) { + // Stored: to a byte boundary, the length and its complement. + r.bits = 0; r.count = 0; + if (r.at + 4 > r.n) { good = false; break; } + const u32 len = u32(r.in[r.at]) | (u32(r.in[r.at + 1]) << 8); + const u32 nlen = u32(r.in[r.at + 2]) | (u32(r.in[r.at + 3]) << 8); + r.at += 4; + if (len != (~nlen & 0xffffu) || r.at + len > r.n || len > cap - at) { good = false; break; } + for (u32 k = 0; k < len; ++k) o[at++] = r.in[r.at++]; + } else if (type == 1) { + good = fixed(r, o, cap, at); + } else if (type == 2) { + good = dynamic(r, o, cap, at); + } else { + good = false; + } + } + if (ok) *ok = good ? 1 : 0; + return at; +} diff --git a/ld/src/load.cpp b/ld/src/load.cpp index 43689cf..dcd1385 100644 --- a/ld/src/load.cpp +++ b/ld/src/load.cpp @@ -406,7 +406,11 @@ void place_block(const okld_host* host, unsigned char* region, elf::u64 at, elf: const elf::u64 tail = round_up(hi - (c + run), g); if (c + run + tail <= span && how_of(c + run) == how) run += tail; } - if (host->map(region, c, run, src.bytes, b.offset + (c - lo), how, host->ctx) == 0) { + // The data an image at a base starts with, read in with it where + // the host reads everything ahead; a snapshot's is read as it is + // touched (a start touches little of what its initialisers made). + const int ahead = host->prefetch >= 2 && !src.snapshot ? OKLD_MAP_PREFETCH : 0; + if (host->map(region, c, run, src.bytes, b.offset + (c - lo), how | ahead, host->ctx) == 0) { mapped = true; c += run; } diff --git a/ld/src/okb.cpp b/ld/src/okb.cpp index 866becd..3e10ff3 100644 --- a/ld/src/okb.cpp +++ b/ld/src/okb.cpp @@ -320,7 +320,7 @@ namespace { bool admit_layout(okld_u64 archive_size, const okld_zip_member* m, char (&name)[256], okld_why* why) { const size n = m->name_len < sizeof name - 1 ? m->name_len : sizeof name - 1; copy(name, m->name, n); name[n] = 0; - if (m->method != 0 || m->size != m->original) { say(why, name, ": not stored; an okb member is stored as it is (docs/okb.md)"); return false; } + if (m->method != 0 || m->size != m->original) { say(why, name, ": not stored; it is expanded before it is admitted (docs/okb.md)"); return false; } if (m->data > archive_size || m->size > archive_size - m->data) { say(why, name, ": its bytes extend beyond the archive"); return false; } if (okld_okb_is_image(name, n) && m->data % OKLD_OKB_ALIGNMENT != 0) { say(why, name, ": its bytes do not begin at a multiple of 64 KiB (docs/okb.md)"); @@ -349,6 +349,31 @@ extern "C" int okld_okb_admit(const void* archive, okld_u64 archive_size, const return 0; } +extern "C" int okld_okb_expand(const void* archive, okld_u64 archive_size, const okld_zip_member* m, + const void* json, okld_size json_n, void* out, okld_u64 cap, okld_why* why) { + char name[256]; + const size n = m->name_len < sizeof name - 1 ? m->name_len : sizeof name - 1; + copy(name, m->name, n); name[n] = 0; + if (m->method != 8) { say(why, name, ": not compressed (DEFLATE, method 8)"); return -1; } + if (m->data > archive_size || m->size > archive_size - m->data) { say(why, name, ": its bytes extend beyond the archive"); return -1; } + if (cap < m->original) { say(why, name, ": the room given is smaller than the member"); return -1; } + int whole = 0; + const u64 got = okld_inflate(static_cast(archive) + m->data, m->size, out, m->original, &whole); + if (!whole || got != m->original) { say(why, name, ": its compressed bytes do not expand to its size"); return -1; } + if (json) { + u8 want[32], have[32]; + const int found = okld_okb_digest(json, json_n, name, want); + if (found < 0) { say(why, "okb.json cannot be read"); return -1; } + if (found == 0) { say(why, name, ": okb.json states no digest for it"); return -1; } + okld_sha256(out, m->original, have); + for (int i = 0; i < 32; ++i) + if (want[i] != have[i]) { say(why, name, ": its content is not the one okb.json states (sha256)"); return -1; } + return 0; + } + if (okld_crc32(0, out, m->original) != m->crc32) { say(why, name, ": its content does not match its CRC-32"); return -1; } + return 0; +} + extern "C" int okld_okb_admit_layout(okld_u64 archive_size, const okld_zip_member* m, okld_why* why) { char name[256]; return admit_layout(archive_size, m, name, why) ? 0 : -1; diff --git a/ld/src/okld.cppm b/ld/src/okld.cppm index f13f109..9f403d4 100644 --- a/ld/src/okld.cppm +++ b/ld/src/okld.cppm @@ -26,6 +26,8 @@ export using ::okld_sign; export using ::okld_verify; export using ::okld_public_key; export using ::okld_load; +export using ::okld_okb_expand; +export using ::okld_inflate; export using ::okld_bound_imports; export using ::okld_initialise; export using ::okld_finalise; diff --git a/ld/src/zip.cpp b/ld/src/zip.cpp index 6ad94f7..51c2965 100644 --- a/ld/src/zip.cpp +++ b/ld/src/zip.cpp @@ -153,7 +153,9 @@ extern "C" okld_u64 okld_zip_write(const okld_zip_input* in, okld_size count, ok for (okld_size i = 0; i < count; ++i) { const auto& m = in[i]; const u64 name_len = length(m.name); - const bool big = m.size >= kMax32; + const bool deflated = m.method == 8; + const u64 original = deflated ? m.original : m.size; + const bool big = m.size >= kMax32 || original >= kMax32; u64 extra = big ? 20 : 0; if (m.align && alignment > 1) { // The bytes begin after 30 + name + extra; the field that pads @@ -170,16 +172,16 @@ extern "C" okld_u64 okld_zip_write(const okld_zip_input* in, okld_size count, ok w.w32(kLocal); w.w16(big ? 45 : 20); w.w16(0); // flags - w.w16(0); // stored + w.w16(deflated ? 8 : 0); // stored, or DEFLATE w.w16(kDosTime); w.w16(kDosDate); - const u32 crc = okld_crc32(0, m.bytes, m.size); + const u32 crc = deflated ? m.crc32 : okld_crc32(0, m.bytes, m.size); w.w32(crc); w.w32(big ? u32(kMax32) : u32(m.size)); - w.w32(big ? u32(kMax32) : u32(m.size)); + w.w32(big ? u32(kMax32) : u32(original)); w.w16(u16(name_len)); w.w16(u16(extra)); w.bytes(m.name, name_len); - if (big) { w.w16(kZip64Field); w.w16(16); w.w64(m.size); w.w64(m.size); extra -= 20; } + if (big) { w.w16(kZip64Field); w.w16(16); w.w64(original); w.w64(m.size); extra -= 20; } if (extra > 0) { w.w16(kAlignField); w.w16(u16(extra - 4)); w.w32(alignment); w.zeros(extra - 8); } w.bytes(m.bytes, m.size); } @@ -192,7 +194,9 @@ extern "C" okld_u64 okld_zip_write(const okld_zip_input* in, okld_size count, ok for (okld_size i = 0; i < count; ++i) { const auto& m = in[i]; const u64 name_len = length(m.name); - const bool big = m.size >= kMax32; + const bool deflated = m.method == 8; + const u64 original = deflated ? m.original : m.size; + const bool big = m.size >= kMax32 || original >= kMax32; u64 extra = big ? 20 : 0; if (m.align && alignment > 1) { u64 r = (alignment - (c.at + 30 + name_len + extra) % alignment) % alignment; @@ -201,17 +205,17 @@ extern "C" okld_u64 okld_zip_write(const okld_zip_input* in, okld_size count, ok } const u64 header = c.at; c.at += 30 + name_len + extra + m.size; - const u32 crc = okld_crc32(0, m.bytes, m.size); + const u32 crc = deflated ? m.crc32 : okld_crc32(0, m.bytes, m.size); const bool big_at = header >= kMax32; const u64 extra_c = (big ? 16 : 0) + (big_at ? 8 : 0); w.w32(kCentral); w.w16((3 << 8) | ((big || big_at) ? 45 : 20)); // made by: UNIX w.w16((big || big_at) ? 45 : 20); - w.w16(0); w.w16(0); + w.w16(0); w.w16(deflated ? 8 : 0); w.w16(kDosTime); w.w16(kDosDate); w.w32(crc); w.w32(big ? u32(kMax32) : u32(m.size)); - w.w32(big ? u32(kMax32) : u32(m.size)); + w.w32(big ? u32(kMax32) : u32(original)); w.w16(u16(name_len)); w.w16(u16(extra_c ? extra_c + 4 : 0)); w.w16(0); w.w16(0); w.w16(0); @@ -220,7 +224,7 @@ extern "C" okld_u64 okld_zip_write(const okld_zip_input* in, okld_size count, ok w.bytes(m.name, name_len); if (extra_c) { w.w16(kZip64Field); w.w16(u16(extra_c)); - if (big) { w.w64(m.size); w.w64(m.size); } + if (big) { w.w64(original); w.w64(m.size); } if (big_at) w.w64(header); } } diff --git a/ld/tests/okb_test.cpp b/ld/tests/okb_test.cpp index eb7b171..e65958b 100644 --- a/ld/tests/okb_test.cpp +++ b/ld/tests/okb_test.cpp @@ -4,7 +4,10 @@ // refusals --- a member not stored, an image not aligned, a content that is not // the stated one, a manifest signature that does not hold. #include +#include #include +#include +#include #include #include #include @@ -135,6 +138,93 @@ int main() { altered[5] = '2'; check(okld_verify_message(key, altered.data(), altered.size(), sig) < 0, "an altered manifest's signature does not hold"); + // DEFLATE (0.4): a fixed block, a dynamic one and a stored one, as zlib + // writes them (raw, level 9 and 0), expand to their bytes; a stream cut + // short does not; and a member stored compressed is expanded and held to + // its digest. + { + auto from_hex = [](std::string_view h) { + std::vector v; + for (std::size_t i = 0; i + 1 < h.size(); i += 2) v.push_back(static_cast(std::stoi(std::string(h.substr(i, 2)), nullptr, 16))); + return v; + }; + const std::string text = "an okb member, compressed"; + // The dynamic block's bytes: 1500 words of nine, chosen by an LCG. + static const char* const words[] = { "okrun", "image", "snapshot", "placed", "base", "kal", "region", "zzz", "q" }; + std::string many; + unsigned x = 12345; + for (int i = 0; i < 1500; ++i) { + x = (x * 1103515245u + 12345u) & 0x7fffffffu; + if (i) many += ' '; + many += words[(x >> 16) % 9]; + } + const auto fixed = from_hex("4bcc53c8cf4e52c84dcd4d4a2dd25148cecf2d284a2d2e4e4d0100"); + const auto stored = from_hex("011900e6ff616e206f6b62206d656d6265722c20636f6d70726573736564"); + const auto dynamic = from_hex( + "85595952dc3010bdca5c6d92508482000dc90fa74fd9dade26532920d6c852afaf5ff7bcbfdc7f3efcba7d7d7ddd7edc" + "3f1f6e1f0f8f4f6fafe7f35cabf6e77d6d3d7e3e5fefef9fbfdffedede9e3ffebdf6dfe746fab4bfd5ff3cfdb93fb60d" + "b5b6d4b9b03e6a47b567b9a52d3edf5ffaf3da3ec40aa2f5bb5524bc562eabb107cc21e21e3f871cedcdbeaf3d2c814e" + "73b87c6d5b811e649e6339a801b2748f9cbfe6ae716afb771cd2dfa02baacb7f7c3e97daab7c9c7c4867b5a5bea32dcd" + "f7fa4636a05bb6a951ee33f20e2acae21df2cf87636bbfe9dc25eea47dbeda1418562fd6ae1f0be6ea2b64105e3b6580" + "37dab12064495a80d0f35d0c4cd107d331f938452dde34e203bc32af2db9461d48b6f4fb40638d8815ed21880abcc0c6" + "1c01521ac77015bfc10ad035a0f14897fec9926e62d8cc608fde95a8152360fa593c87d90ffe26cd5441f18ac860be6c" + "db179aa2e333a21407d150bd3d15811a03147a9354e85176e59618981a3243455d4fe195d16865120b8d29e4b1cd9ea0" + "93f137994d70581e0944c1b11285a1d0609952f8103762c2261ca443e8aa8be890b24f376fab90e8910c098715668182" + "67e93aec2d4902f634fab8380797e0b15c652ea2005601a3bdcc6b5683f9939d033aac5fe5640bb333a589bb99a216ce" + "8022d0e28e60a5df8fd9aa5837ff93cbac5140b3d1e025096bc0eb9b34950f8d60aab8f2f18a4a4a934d4957bb3838c0" + "9550d4808509c1da111ac12c38d24d5cc38999268562549bd0f13a1c8a9d0006b97d956ded2b92273d4a6117981d68ca" + "ca77cef385089ce8cbcab1545b3ab01d01ad26068161e65a80bf15d1e6e2a58372d092ae6bf2363024e85214a7e29769" + "34291719efafde51a443e1166614b72a217020bcb75e07398a924433708071aa78040d81e265ba392d143b51bd3f7452" + "c2fdb3c4683cd8bc72690615d3a90d4dac80076cdc650578c4b841c36ff15b1b6f1c116493a215e7b63dec8c24e0823e" + "b56ce1141adc91f8462f38b728090d63b5f362d36f602ad356e14078bfc6be375039ba8cb6ac057857a903e564626b6d" + "4354859b8185bc251c6868666228c429908f632c0314d9dcf6055645a3546aae0ccb220d14d2a3a42df1ed2e4af21dbb" + "00f02923e837913172c3f230890c1b8962053e604d56a8e6c5f83a44110ac5aa5680d434adc800a3b008c6615c4c2c5b" + "ad8a876530dece4ab6c343e9c8942066529ad21a5367d14fc85cb296cceeac738f425fcfe46c282285923495f9143563" + "b505589aa98481626a3662d7422d0a5a6d710d27850a1bc057ac3645dd6d46a45d0a447ad04e8bcaa6b558dbbe1b4ca4" + "d82be7f29723124a588e8cf21174c0b47d72527146f0bfcac3d05952b6a8acde7b63396962d056026f3b08f22d026ced" + "e719c26c486fcb2d37c42a066442b54f9a20095727bb873149a2ec64634ac238d622d952a697392fcdaa40db21629a81" + "6e3a7d9b774e0b585340f266cf645aee28022113a20bbf3a108784dcf1410c6b28d1106a541ec85fcc1e930d88e34a30" + "c552906a86f002c3c02b548f0c7dd704a641195230de5691139566ab7f9797138fbfa2bcd0c2dc562b44cd3606127970" + "4afd348c90625d177f5cf8c21d4f0894e2338c90a4750bd3ac34a880b784f46cb930d865df3d521f5af295c6deb5b543" + "9e4df46674ab100f107c3abe3762a3f3c32d5bb7bc91e6a0ac21da160e3cf65cfe0f"); + auto expands = [&](const std::vector& in, const std::string& want) { + std::vector out(want.size() + 16); + int ok = 0; + const okld_u64 n = okld_inflate(in.data(), in.size(), out.data(), out.size(), &ok); + return ok == 1 && n == want.size() && std::equal(want.begin(), want.end(), out.begin()); + }; + check(expands(fixed, text), "a fixed DEFLATE block expands to its bytes"); + check(expands(dynamic, many), "a dynamic one does"); + check(expands(stored, text), "and a stored one"); + { + std::vector out(64); + int ok = 1; + okld_inflate(dynamic.data(), dynamic.size() / 2, out.data(), out.size(), &ok); + check(ok == 0, "a stream cut short does not expand"); + } + // An archive whose member is the dynamic stream, method 8. + const std::string json = "{ \"okb\": 1, \"name\": \"z\", \"entry\": \"z.oke\", \"architectures\": {}, \"files\": { \"x86_64/z.oke\": { \"sha256\": \"" + + [&] { unsigned char d[32]; okld_sha256(many.data(), many.size(), d); std::string h; for (unsigned char c : d) h += std::format("{:02x}", c); return h; }() + + "\", \"size\": " + std::to_string(many.size()) + " } } }"; + std::vector in; + in.push_back({ "okb.json", json.data(), json.size(), 0, 0, 0, 0 }); + in.push_back({ "x86_64/z.oke", dynamic.data(), dynamic.size(), 0, 8, many.size(), okld_crc32(0, many.data(), many.size()) }); + const auto z = write(in); + okld_zip_member m{}; + check(okld_zip_find(z.data(), z.size(), "x86_64/z.oke", &m, &why) == 0 && m.method == 8 && m.original == many.size(), + "a member is written compressed, its size as expanded stated"); + std::vector e(m.original); + okld_zip_member j{}; + okld_zip_find(z.data(), z.size(), "okb.json", &j, &why); + check(okld_okb_expand(z.data(), z.size(), &m, z.data() + j.data, j.size, e.data(), e.size(), &why) == 0 + && std::equal(many.begin(), many.end(), e.begin()), "and expanded, held to the digest okb.json states"); + check(okld_okb_admit(z.data(), z.size(), &m, z.data() + j.data, j.size, &why) != 0, "it is not admitted as it is stored"); + std::vector damaged = z; + damaged[m.data + m.size / 2] ^= 0x55; + check(okld_okb_expand(damaged.data(), damaged.size(), &m, damaged.data() + j.data, j.size, e.data(), e.size(), &why) != 0, + "a changed byte of it is refused"); + } + // ZIP64: more members than the 16-bit count holds. { std::vector names(70000); diff --git a/run/include/okrun.h b/run/include/okrun.h index b38c894..3934b48 100644 --- a/run/include/okrun.h +++ b/run/include/okrun.h @@ -106,6 +106,19 @@ void okrun_close(struct okrun_library* library); * null measures nothing. Without a call, OKRUN_PROF is read. */ void okrun_profile(const char* spec); +/* --- what the runner keeps for this machine (0.4) ------------------------- */ + +/* Each file of the runner's cache --- a base for each boot, an image's data + * prepared for it (`.okseg'), a snapshot of its initialisers (`.oksnap') and its + * marker (`.sealing') --- with its size, and whether it was prepared for this + * boot; returns the bytes they hold. */ +unsigned long long okrun_cache_list(void (*each)(const char* name, unsigned long long bytes, int this_boot, void* ctx), + void* ctx); +/* Removes what was prepared for another boot, or (`all' non-zero) everything + * the runner keeps, its remembered admissions and digests included; returns the + * bytes removed. */ +unsigned long long okrun_cache_reclaim(int all); + /* The names this runner binds (the specification's surface), `count' of them. */ const char* const* okrun_names(unsigned long* count); diff --git a/run/src/okrun.cppm b/run/src/okrun.cppm index 8efe8b0..bd93f40 100644 --- a/run/src/okrun.cppm +++ b/run/src/okrun.cppm @@ -18,3 +18,5 @@ export using ::okrun_open_library_bytes; export using ::okrun_symbol; export using ::okrun_close; export using ::okrun_profile; +export using ::okrun_cache_list; +export using ::okrun_cache_reclaim; diff --git a/run/src/prof.cpp b/run/src/prof.cpp index 4b2f898..0e7543c 100644 --- a/run/src/prof.cpp +++ b/run/src/prof.cpp @@ -478,6 +478,19 @@ bool profile_calls() { return (mode() & asked_calls) != 0; } bool profile_samples() { return (mode() & asked_samples) != 0; } bool profile_touches() { return (mode() & asked_touch) != 0; } +// WHAT THE RUNNER CHOSE AT EACH POINT A RESOURCE IS SPENT (0.4, mcpp design +// 2026-11-03 part 4 §16): the placement's data, the snapshot, what is read +// ahead --- each with its outcome and the bytes it holds. +struct note { const char* point; const char* outcome; kal_u64 bytes; }; +note g_notes[8]; +unsigned g_note_count = 0; + +void profile_note(const char* point, const char* outcome, kal_u64 bytes) { + lock(); + if (g_note_count < 8) g_notes[g_note_count++] = note{ point, outcome, bytes }; + unlock(); +} + void profile_phase(const char* name) { if (!(mode() & asked_phases)) return; lock(); @@ -659,6 +672,16 @@ void profile_finish() { b.put(" }"); } b.put(g_moment_count ? "\n ]" : "]"); + // WHAT THE RUNNER CHOSE (0.4). + b.put(",\n \"runner\": ["); + for (unsigned i = 0; i < g_note_count; ++i) { + b.put(i ? ",\n { \"point\": " : "\n { \"point\": "); + b.quoted(g_notes[i].point); + b.put(", \"outcome\": "); b.quoted(g_notes[i].outcome); + b.put(", \"bytes\": "); b.number(g_notes[i].bytes); + b.put(" }"); + } + b.put(g_note_count ? "\n ]" : "]"); // SAMPLES, AS STACKS (0.4): the outermost frame first, `;' between, as // a folded stack is written. // A TIMELINE beside them: each sample's time, from the start, and its diff --git a/run/src/run.cpp b/run/src/run.cpp index 1016632..0335dec 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -278,12 +278,18 @@ struct search { char ran_path[1024] = {}; char needed_path[16][1024] = {}; char needed_name[16][256] = {}; + // A member of the archive expanded into a file of the cache (0.4): its + // name in the archive, beside the file it is held in. + char needed_member[16][256] = {}; }; // The file a run's image or okb was read from, given back once the image is // loaded: its pages are no longer resident beside the image's own. void release_ran(search& s) { if (s.prepared.data) release_file(s.prepared); + // A member expanded for the entry, held until the image was loaded. + for (unsigned k = 0; k < 16; ++k) + if (s.needed[k].data && s.needed_member[k][0]) { release_file(s.needed[k]); s.needed_member[k][0] = 0; } if (!s.ran.data) return; if (s.archive == s.ran.data) { s.archive = nullptr; s.archive_size = 0; s.json = nullptr; s.json_size = 0; } release_file(s.ran); @@ -490,12 +496,15 @@ memo_place g_memo; bool g_memo_opened = false; // A member of the archive by name, admitted (stored, aligned, its stated content). +const void* expanded_member(search& s, const okld_zip_member& m, const char* member, unsigned long long& size); + const void* archive_member(search& s, const char* member, unsigned long long& size) { okld_zip_member m{}; okld_why why{}; if (okld_zip_find(s.archive, s.archive_size, member, &m, &why) != 0) return nullptr; copy_text(s.member, sizeof s.member, member); if (!g_memo_opened) { g_memo_opened = true; memo_open(s, g_memo); } + if (m.method == 8) return expanded_member(s, m, member, size); if (memo_holds(g_memo, member)) { // Compared before, on this file as it is: its shape alone is asked, // and no byte of it is read (0.3.1: the CRC-32 the archive states was @@ -577,9 +586,11 @@ const void* find_needed(const char* name, okld_size* size, void* ctx) { unsigned long long n = 0; for (unsigned long i = 0; !bytes && i < s.image_count; ++i) if (same(s.images[i].name, name)) { bytes = s.images[i].bytes; n = s.images[i].size; } + bool expanded = false; if (!bytes && s.archive) { bytes = archive_image(s, name, n); if (!bytes && s.refusal[0]) return nullptr; + for (const auto& h : s.needed) expanded = expanded || (bytes && h.data == bytes); } file_bytes f; char found[1024] = {}; @@ -600,6 +611,8 @@ const void* find_needed(const char* name, okld_size* size, void* ctx) { *slot = f; copy_text(s.needed_path[k], sizeof s.needed_path[k], found); copy_text(s.needed_name[k], sizeof s.needed_name[k], name); + } else if (expanded) { + // Held in its slot, and named there, until the loader gives it back. } else if (s.archive && bytes >= s.archive && bytes < s.archive + s.archive_size) { profile_loaded(name, bytes, n, s.ran_path[0] ? s.ran_path : nullptr, static_cast(static_cast(bytes) - s.archive)); @@ -622,6 +635,7 @@ void release_needed(const void* bytes, okld_size, void* ctx) { profile_loaded(s.needed_name[k], h.data, h.size, s.needed_path[k], 0); profile_settle(); release_file(h); + s.needed_member[k][0] = 0; return; } } @@ -662,21 +676,51 @@ int seal(void* region, okld_size offset, okld_size size, int forever, void*) { return kal_exec_seal_part(region, offset, size, forever ? KAL_EXEC_SEAL_FOREVER : 0) == kal_ok ? 0 : -1; } -// WHETHER THE PAGES A START TOUCHES ARE READ IN AHEAD OF IT (0.4): memory spent -// for time, as the program's resource preference says (openkal 0.23) --- always -// for performance, never for economy, and where memory is plentiful (a quarter -// of it available) when balanced. -bool prefetching() { +// WHAT THIS START SPENDS RESOURCES FOR (0.4, openkal 0.23; mcpp design +// 2026-11-03 part 4 §16): the program's preference, and balanced taken as +// economy where what it would spend runs short --- memory below 15 % of the +// machine's available, or the volume of the runner's cache below 1 GiB free. +enum class spend { performance, balanced, economy }; + +spend preference() { switch (kal_env_resources()) { - case KAL_RESOURCES_PERFORMANCE: return true; - case KAL_RESOURCES_ECONOMY: return false; - default: { - kal_memory_status m{}; - m.self_size = sizeof m; - if (kal_memory_info(&m) != kal_ok || !(m.present & KAL_MSTATUS_AVAILABLE) || !(m.present & KAL_MSTATUS_TOTAL)) - return false; - return m.available_bytes >= m.total_bytes / 4; - } + case KAL_RESOURCES_PERFORMANCE: return spend::performance; + case KAL_RESOURCES_ECONOMY: return spend::economy; + default: return spend::balanced; + } +} + +spend memory_spend() { + const spend p = preference(); + if (p != spend::balanced) return p; + kal_memory_status m{}; + m.self_size = sizeof m; + if (kal_memory_info(&m) != kal_ok || !(m.present & KAL_MSTATUS_AVAILABLE) || !(m.present & KAL_MSTATUS_TOTAL)) + return spend::balanced; + return m.available_bytes * 100 < m.total_bytes * 15 ? spend::economy : spend::balanced; +} + +spend disk_spend(kal_dir cache) { + const spend p = preference(); + if (p != spend::balanced || !(kal_fs_props(cache) & KAL_FS_PROP_CAPACITY)) return p; + kal_u64 total = 0, available = 0; + if (kal_fs_capacity(cache, &total, &available) != kal_ok) return p; + return available < (1ull << 30) ? spend::economy : spend::balanced; +} + +// WHAT IS READ IN AHEAD OF A START (okld_host::prefetch): the pages it touches, +// and for performance the data of an image at its base too; nothing for +// economy. OKRUN_PREFETCH=0, `touch' or `all' states it. +int prefetching() { + char v[8]; + const kal_intptr n = kal_env_var("OKRUN_PREFETCH", 14, v, sizeof v); + if (n == 1 && v[0] == '0') return 0; + if (n == 5 && same_bytes(v, "touch", 5)) return 1; + if (n == 3 && same_bytes(v, "all", 3)) return 2; + switch (memory_spend()) { + case spend::performance: return 2; + case spend::economy: return 0; + default: return 1; } } @@ -693,7 +737,7 @@ void prepare_host(search& s, bool permanent) { host.ctx = &s; host.seal = &seal; host.permanent = permanent ? 1 : 0; - host.prefetch = prefetching() ? 1 : 0; + host.prefetch = prefetching(); } @@ -780,6 +824,72 @@ bool write_whole(kal_dir dir, const char* name, const void* bytes, unsigned long return true; } +// A MEMBER STORED COMPRESSED (docs/okb.md, 0.4): expanded once into the +// user's cache (`okrun/expanded', named by the manifest's digest and the +// member), held to the digest the manifest states, and mapped from there as +// an image's own file is --- its instructions the file's pages. A later start +// of the same okb file, whose admission is remembered, maps it again without +// expanding. Held in a slot of `needed' until the image is loaded. +const void* expanded_member(search& s, const okld_zip_member& m, const char* member, unsigned long long& size) { + static const char* const below[] = { "okrun", "expanded" }; + owned_dir dir; + if (!s.json || !user_cache(below, 2, dir.d, &dir.own)) { + copy_text(s.refusal, sizeof s.refusal, "a compressed member is expanded into the user's cache, which cannot be had"); + return nullptr; + } + unsigned k = 0; + while (k < 16 && s.needed[k].data) ++k; + if (k == 16) { copy_text(s.refusal, sizeof s.refusal, "more than sixteen files are needed at once"); return nullptr; } + char name[96]; + { + unsigned char j[32], d[32]; + okld_sha256(s.json, s.json_size, j); + okld_sha256(member, length(member), d); + char hj[33], hd[33]; + to_hex(j, 16, hj); + to_hex(d, 16, hd); + const unsigned long n = length(member); + const bool library = n > 5 && same_bytes(member + n - 5, ".okso", 5); + text(name, sizeof name) << hj << "-" << hd << (library ? ".okso" : ".oke"); + } + auto map_expanded = [&]() -> bool { + kal_file f{}; + if (kal_fs_open(dir.d, name, length(name), KAL_OPEN_READ, &f) != kal_ok) return false; + file_bytes fb; + if (!map_all(f, fb)) { kal_fs_close_file(f); return false; } + if (fb.size != m.original) { release_file(fb); return false; } + s.needed[k] = fb; + return true; + }; + bool held = memo_holds(g_memo, member) && map_expanded(); + if (!held) { + void* out = kal_alloc(m.original ? m.original : 1, 16); + if (!out) { copy_text(s.refusal, sizeof s.refusal, "no memory to expand a compressed member"); return nullptr; } + okld_why why{}; + const bool good = okld_okb_expand(s.archive, s.archive_size, &m, s.json, s.json_size, out, m.original, &why) == 0 + && write_whole(dir.d, name, out, m.original); + if (!good) copy_text(s.refusal, sizeof s.refusal, why.text[0] ? why.text : "a compressed member could not be expanded"); + kal_free(out, m.original ? m.original : 1, 16); + if (!good || !map_expanded()) return nullptr; + memo_record(g_memo, member); + profile_note("okb", "a compressed member expanded into the cache", m.original); + } + // Where the expanded file is, for a measurement and openkal.debug. + char where[1024]; + { + text t(where, sizeof where); + char base[1024]; + const kal_intptr n = kal_fs_resolved_name(dir.d, ".", 1, base, sizeof base - 1); + if (n > 0 && n < static_cast(sizeof base)) { base[n] = 0; t << base << "/" << name; } + else t << name; + } + copy_text(s.needed_path[k], sizeof s.needed_path[k], where); + copy_text(s.needed_name[k], sizeof s.needed_name[k], member); + copy_text(s.needed_member[k], sizeof s.needed_member[k], member); + size = m.original; + return s.needed[k].data; +} + unsigned long long parse_number(const char*& p) { unsigned long long v = 0; while (*p >= '0' && *p <= '9') v = v * 10 + static_cast(*p++ - '0'); @@ -831,6 +941,9 @@ bool image_digest(search& s, const void* bytes, unsigned long long size, unsigne const auto* b = static_cast(bytes); if (s.json && s.archive && b >= s.archive && b < s.archive + s.archive_size && s.member[0]) return okld_okb_digest(s.json, s.json_size, s.member, out) == 1; + for (unsigned k = 0; s.json && k < 16; ++k) + if (s.needed[k].data == b && s.needed_member[k][0]) + return okld_okb_digest(s.json, s.json_size, s.needed_member[k], out) == 1; if (!s.ran_path[0] || !s.ran.data || b < s.ran.data || b >= s.ran.data + s.ran.size) return false; kal_dir dir{}; const char* rest = nullptr; @@ -891,6 +1004,15 @@ struct local_place { char stem[64] = {}; }; +// Why an image is not at a base of this boot: what the policy or the image says. +const char* placementless() { + switch (placement_policy()) { + case policy::sealed: return "the sealed base (OKRUN_PLACE=sealed)"; + case policy::anywhere: return "anywhere (OKRUN_PLACE=0)"; + default: return "the sealed base or anywhere: no base of this boot"; + } +} + bool local_place_of(search& s, const void* bytes, unsigned long long size, local_place& lp) { if (placement_policy() != policy::local || !s.host.reserve_at) return false; okld_u64 sealed = 0; okld_u32 count = 0; @@ -939,17 +1061,32 @@ bool map_prepared(const local_place& lp, const char* suffix, file_bytes& out, ok bool prepare_local(search& s, const void* bytes, unsigned long long size, const local_place& lp) { okld_prepared_info info{}; auto plain = [](const okld_prepared_info& i, const local_place&) { return i.flags == 0; }; - if (map_prepared(lp, ".okseg", s.prepared, info, plain)) return true; + if (map_prepared(lp, ".okseg", s.prepared, info, plain)) { + profile_note("placement", "the boot's prepared copy", s.prepared.size); + return true; + } okld_why why{}; const okld_u64 need = okld_prepare(bytes, size, lp.base, lp.digest, nullptr, 0, &why); if (need == 0) return false; void* buffer = kal_alloc(need, 16); if (!buffer) return false; + if (okld_prepare(bytes, size, lp.base, lp.digest, buffer, need, &why) != need) { kal_free(buffer, need, 16); return false; } + // FOR ECONOMY, NOTHING WRITTEN (§16): the data for this boot's base is + // computed for this start and copied from memory, as an image anywhere is + // relocated at every start. + if (disk_spend(lp.dir.d) == spend::economy) { + s.prepared = file_bytes{}; + s.prepared.data = static_cast(buffer); + s.prepared.size = need; + s.prepared.cap = need; + profile_note("placement", "computed for this start", need); + return true; + } char name[96]; place_name(lp, ".okseg", name); - const bool made = okld_prepare(bytes, size, lp.base, lp.digest, buffer, need, &why) == need - && write_whole(lp.dir.d, name, buffer, need); + const bool made = write_whole(lp.dir.d, name, buffer, need); kal_free(buffer, need, 16); + if (made) profile_note("placement", "prepared for this boot", need); return made && map_prepared(lp, ".okseg", s.prepared, info, plain); } @@ -1006,8 +1143,22 @@ bool snapshot_found(search& s, const local_place& lp, okld_prepared_info& info) if (!map_prepared(lp, ".oksnap", s.prepared, info, made_here)) return false; if (info.kept_size && !seal_kept_holds(s.prepared.data + info.kept, info.kept_size)) { release_file(s.prepared); + profile_note("snapshot", "declined: a named value the initialisers read differs", 0); return false; } + // USED: its time is the last use, by which the least recently used is + // removed first when the snapshots outgrow their bound (written at most + // once an hour). + kal_node_info node{}; + node.self_size = sizeof node; + const kal_u64 now = kal_time_wall(); + if (kal_fs_file_info(s.prepared.file, KAL_INFO_MODIFIED, &node) == kal_ok && (node.present & KAL_INFO_MODIFIED) + && node.modified_ns + 3600ull * 1000000000ull < now) { + char name[96]; + place_name(lp, ".oksnap", name); + kal_fs_set_modified_at(lp.dir.d, name, length(name), now); + } + profile_note("snapshot", "used", s.prepared.size); return true; } @@ -1077,9 +1228,9 @@ bool start_again(const char* value, kal_process& out) { // ONCE IN A BOOT FOR AN IMAGE: the marker is made exclusively, and only the // start that made it starts the runner that makes the snapshot. Not for a -// program that asked to spend nothing ahead of it (openkal 0.23, economy). +// program that spends nothing ahead of it (economy, §16). void start_making(const local_place& lp) { - if (kal_env_resources() == KAL_RESOURCES_ECONOMY) return; + if (disk_spend(lp.dir.d) == spend::economy) return; char name[96]; place_name(lp, ".sealing", name); kal_file f{}; @@ -1216,6 +1367,48 @@ void prove_range(kal_u64 from, kal_u64 to, void* ctx) { } } +// THE SNAPSHOTS' BOUND (§16): 2 GiB for performance, 512 MiB balanced; the +// least recently used are removed until a new one of `bytes' fits, each +// marker left saying so, so that it is not made again in the boot. +void make_room(const local_place& lp, kal_u64 bytes) { + const kal_u64 bound = preference() == spend::performance ? (2ull << 30) : (512ull << 20); + if (bytes > bound) seal_refused("the snapshot is larger than the snapshots' bound"); + struct held { char name[96]; kal_u64 size, used; } snaps[64]; + unsigned count = 0; + kal_u64 total = 0; + kal_uintptr iter = 0; + if (kal_fs_list_begin(lp.dir.d, &iter) != kal_ok) return; + while (iter != 0 && count < 64) { + char name[96]; kal_uintptr n = 0; + kal_node_info info{}; + info.self_size = sizeof info; + int kind = 0; + if (kal_fs_list_next(lp.dir.d, &iter, name, sizeof name - 1, &n, &kind) != kal_ok || iter == 0) break; + if (n < 8 || n >= sizeof name || !same_bytes(name + n - 7, ".oksnap", 7)) continue; + if (kal_fs_info(lp.dir.d, name, n, 0, KAL_INFO_SIZE | KAL_INFO_MODIFIED, &info) != kal_ok) continue; + for (kal_uintptr k = 0; k < n; ++k) snaps[count].name[k] = name[k]; + snaps[count].name[n] = 0; + snaps[count].size = info.size; + snaps[count].used = info.modified_ns; + total += info.size; + ++count; + } + while (total + bytes > bound && count > 0) { + unsigned oldest = 0; + for (unsigned i = 1; i < count; ++i) if (snaps[i].used < snaps[oldest].used) oldest = i; + const auto& o = snaps[oldest]; + kal_fs_remove(lp.dir.d, o.name, length(o.name)); + char marker[96]; + text(marker, sizeof marker) << o.name; + const unsigned long stem = length(marker) - 7; + text(marker + stem, sizeof marker - stem) << ".sealing"; + static const char line[] = "removed: the least recently used, beyond the snapshots' bound\n"; + write_whole(lp.dir.d, marker, line, sizeof line - 1); + total -= o.size; + snaps[oldest] = snaps[--count]; + } +} + [[noreturn]] void make_snapshot(search& s, const void* bytes, unsigned long long size, const char* name) { local_place lp; if (!local_place_of(s, bytes, size, lp)) seal_refused("the image is not placed at a base of this boot"); @@ -1315,6 +1508,7 @@ void prove_range(kal_u64 from, kal_u64 to, void* ctx) { *record = snapshot_record{ arena_at, used, 0, 0, kSealContext }; char file[96]; place_name(lp, ".oksnap", file); + make_room(lp, total); if (okld_prepare_blocks(&info, l.b, live, nullptr, kept, kept_size, out, total) != total || !write_whole(lp.dir.d, file, out, total)) seal_refused("its file cannot be written"); @@ -1348,6 +1542,8 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam const snapshotting snapshot = snapshot_mode(); if (snapshot == snapshotting::make) make_snapshot(s, bytes, size, name); profile_phase("admitted"); + profile_note("prefetch", s.host.prefetch == 2 ? "the touched pages and the data" + : s.host.prefetch == 1 ? "the touched pages" : "nothing", 0); okld_module* m = nullptr; okld_why reason{}; // FROM THE SNAPSHOT OF THIS BOOT'S BASE, where there is one; at that base @@ -1367,7 +1563,10 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason); if (loaded != 0) { release_file(s.prepared); reason = okld_why{}; } } - if (loaded != 0) loaded = okld_load(bytes, size, name, &s.host, &m, &reason); + if (loaded != 0) { + profile_note("placement", local ? "this boot's base could not be had" : placementless(), 0); + loaded = okld_load(bytes, size, name, &s.host, &m, &reason); + } if (loaded != 0) { if (s.refusal[0]) text(why, cap) << name << ": " << s.refusal; else text(why, cap) << name << ": " << reason.text; @@ -1381,7 +1580,9 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam { const auto* b = static_cast(bytes); const bool fromRan = s.ran.data && b >= s.ran.data && b < s.ran.data + s.ran.size; - profile_loaded(name, bytes, size, fromRan && s.ran_path[0] ? s.ran_path : nullptr, + const char* expanded = nullptr; + for (unsigned k = 0; k < 16; ++k) if (s.needed[k].data == b && s.needed_member[k][0]) expanded = s.needed_path[k]; + profile_loaded(name, bytes, size, expanded ? expanded : fromRan && s.ran_path[0] ? s.ran_path : nullptr, fromRan ? static_cast(b - s.ran.data) : 0); } profile_phase("loaded"); @@ -1389,8 +1590,12 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam release_ran(s); // An image whose C library takes part, at this boot's base and without a // snapshot, has one made for the starts after this one. - if (local && snapshot == snapshotting::use && !from_snapshot && okld_symbol(m, kSnapshotRecord)) + if (local && snapshot == snapshotting::use && !from_snapshot && okld_symbol(m, kSnapshotRecord)) { + profile_note("snapshot", "none for this boot", 0); start_making(lp); + } else if (snapshot == snapshotting::off) { + profile_note("snapshot", "not used (OKRUN_SNAPSHOT=0)", 0); + } profile_begin_run(); if (from_snapshot) { // THE SECOND PHASE: the C library takes what this start supplies. A @@ -1599,6 +1804,86 @@ extern "C" int okrun_run_images(const okrun_image* images, unsigned long count, return -1; } +// --- what the runner keeps for this machine (0.4) --------------------------------- + +namespace { + +// Whether the cache's boot record is this boot's (boot_slide's rule). +bool this_boot(kal_dir dir) { + const unsigned long long began = kal_time_wall() - kal_time_monotonic(); + char have[128]; + if (read_small(dir, "boot", have, sizeof have) <= 0) return false; + const char* p = have; + parse_number(p); + if (*p == ' ') ++p; + const unsigned long long b0 = parse_number(p); + const unsigned long long apart = b0 > began ? b0 - began : began - b0; + return apart <= 2000000000ull; +} + +bool prepared_name(const char* name, kal_uintptr n) { + return (n > 6 && same_bytes(name + n - 6, ".okseg", 6)) || (n > 7 && same_bytes(name + n - 7, ".oksnap", 7)) + || (n > 8 && same_bytes(name + n - 8, ".sealing", 8)); +} + +// Each file of a directory, with its size, through `each'; `remove' removes +// those it answers true for. Returns the bytes of those answered true. +template +unsigned long long each_file(kal_dir dir, F&& f) { + unsigned long long bytes = 0; + kal_uintptr iter = 0; + if (kal_fs_list_begin(dir, &iter) != kal_ok) return 0; + while (iter != 0) { + char name[256]; kal_uintptr n = 0; int kind = 0; + if (kal_fs_list_next(dir, &iter, name, sizeof name - 1, &n, &kind) != kal_ok || iter == 0) break; + if (n == 0 || n >= sizeof name) continue; + name[n] = 0; + kal_node_info info{}; + info.self_size = sizeof info; + if (kal_fs_info(dir, name, n, 0, KAL_INFO_SIZE | KAL_INFO_KIND, &info) != kal_ok || info.kind != kal_node_file) continue; + if (f(name, n, info.size)) bytes += info.size; + } + return bytes; +} + +} // namespace + +extern "C" unsigned long long okrun_cache_list(void (*each)(const char*, unsigned long long, int, void*), void* ctx) { + owned_dir dir; + if (!cache_directory(dir)) return 0; + const bool current = this_boot(dir.d); + return each_file(dir.d, [&](const char* name, kal_uintptr, kal_u64 size) { + if (each) each(name, size, current ? 1 : 0, ctx); + return true; + }); +} + +extern "C" unsigned long long okrun_cache_reclaim(int all) { + unsigned long long bytes = 0; + owned_dir dir; + if (cache_directory(dir)) { + const bool current = this_boot(dir.d); + bytes += each_file(dir.d, [&](const char* name, kal_uintptr n, kal_u64) { + if (!(all || (!current && prepared_name(name, n)))) return false; + return kal_fs_remove(dir.d, name, n) == kal_ok; + }); + } + if (all) { + static const char* const admitted[] = { "okrun", "admitted" }; + static const char* const digests[] = { "okrun", "digests" }; + static const char* const expanded[] = { "okrun", "expanded" }; + const char* const* const kept[] = { admitted, digests, expanded }; + for (const char* const* below : kept) { + owned_dir d; + if (!user_cache(below, 2, d.d, &d.own)) continue; + bytes += each_file(d.d, [&](const char* name, kal_uintptr n, kal_u64) { + return kal_fs_remove(d.d, name, n) == kal_ok; + }); + } + } + return bytes; +} + // --- libraries ------------------------------------------------------------------ struct okrun_library { diff --git a/run/src/runner.h b/run/src/runner.h index 76d3bdf..ee82837 100644 --- a/run/src/runner.h +++ b/run/src/runner.h @@ -34,6 +34,9 @@ bool profile_touches(); // `touch': this runner records them (0.4) // admitted, loaded, a snapshot mapped, initialised, main returned --- with the // time and the page faults so far. void profile_phase(const char* name); +// What the runner chose at a point a resource is spent (0.4): the placement's +// data, the snapshot, what is read ahead. +void profile_note(const char* point, const char* outcome, kal_u64 bytes); // What a measurement takes once the image is loaded and before it runs: the // samples begun, the touches of its executable parts begun (`okld_iterate'). void profile_begin_run(); diff --git a/tools/mcpp.toml b/tools/mcpp.toml index 4f25d9b..1de3a6b 100644 --- a/tools/mcpp.toml +++ b/tools/mcpp.toml @@ -11,6 +11,11 @@ repo = "https://github.com/mcpplibs/openkal-image" openkal-run = { path = "../run" } openkal-ld = { path = "../ld" } +# The compressor of `okld bundle --compress' (0.4): zlib's DEFLATE. The runner +# expands with libokld's own (freestanding) reader. +[dependencies.compat] +zlib = "1.3.2" + [targets.okrun] kind = "bin" main = "src/okrun.cpp" diff --git a/tools/src/okld.cpp b/tools/src/okld.cpp index 9e62640..15bafb2 100644 --- a/tools/src/okld.cpp +++ b/tools/src/okld.cpp @@ -28,11 +28,15 @@ // okld bundle [--name ] [--version ] [--entry ] [--sign ] ... // an okb (docs/okb.md): the images, by architecture, and // okb.json; with --sign, each image and okb.json signed +// (--compress: each image stored compressed, DEFLATE, which a +// runner expands once into its cache: an okb kept small, 0.4) // okld bundle check [--require-signature] [--trust ]... -// every member stored, every image aligned, every digest, -// signature and image of the okb; one line per violation +// every member stored or an image compressed, every stored +// image aligned, every digest, signature and image of the +// okb; one line per violation // // It does not link: an image is linked by an ordinary linker (`ld.lld -shared'). +#include import std; import openkal.ld; import openkal.run; @@ -216,11 +220,12 @@ std::string base_name(const std::string& path) { int bundle(const std::vector& args) { // okld bundle [--name ] [--version ] [--entry ] [--sign ] ... // okld bundle --package [--sign ] - const char* usage = "usage: okld bundle [--name ] [--version ] [--entry ] [--sign ] ...\n" - " okld bundle --package [--sign ]"; + const char* usage = "usage: okld bundle [--name ] [--version ] [--entry ] [--sign ] [--compress] ...\n" + " okld bundle --package [--sign ] [--compress]"; if (args.size() < 3) { std::println(std::cerr, "{}", usage); return 2; } const std::string out = args[1]; std::string name, version, entry, seed_file, package; + bool compress = false; std::vector paths; std::map expected_arch; // path -> the architecture its place names std::map stated_digest; // path -> the sha256 mcpp-package.json states @@ -237,6 +242,7 @@ int bundle(const std::vector& args) { else if (a == "--entry") { if (!value(entry)) return 2; } else if (a == "--sign") { if (!value(seed_file)) return 2; } else if (a == "--package") { if (!value(package)) return 2; } + else if (a == "--compress") compress = true; else if (a.starts_with("--")) { std::println(std::cerr, "okld: unknown option {}", a); return 2; } else paths.push_back(a); } @@ -332,7 +338,11 @@ int bundle(const std::vector& args) { if (name.empty()) name = entry.substr(0, entry.rfind('.')); // okb.json: keys in a fixed order, files by member name. - std::string json = std::format("{{\n \"okb\": {},\n \"name\": {},\n", okld::okb_version, json_string(name)); + // Version 1 where every member is stored, which every reader reads; 2 where + // an image is compressed, which a reader of 1 refuses by its version rather + // than by a member it cannot admit. + const unsigned okb = compress ? 2u : 1u; + std::string json = std::format("{{\n \"okb\": {},\n \"name\": {},\n", okb, json_string(name)); if (!version.empty()) json += std::format(" \"version\": {},\n", json_string(version)); json += std::format(" \"entry\": {},\n \"architectures\": {{\n", json_string(entry)); { @@ -350,7 +360,7 @@ int bundle(const std::vector& args) { // AN IMAGE'S CHUNKS TOO (docs/okb.md, 0.1.8): a runner verifies them // in parallel, where one SHA-256 of the whole cannot be shared out. std::string chunks; - if (okld_okb_is_image(k.data(), k.size())) { + if (okld_okb_is_image(k.data(), k.size()) && !compress) { chunks = std::format(",\n \"chunks\": {{ \"size\": {}, \"sha256\": [", okld::okb_chunk); for (std::size_t at = 0; at < m.bytes.size(); at += okld::okb_chunk) { unsigned char c[32]; @@ -367,19 +377,40 @@ int bundle(const std::vector& args) { std::array signature{}; std::vector in; - in.push_back({ "okb.json", json.data(), json.size(), 0 }); + in.push_back({ "okb.json", json.data(), json.size(), 0, 0, 0, 0 }); if (seed) { okld_public_key(seed->data(), signature.data()); okld_sign_message(seed->data(), json.data(), json.size(), signature.data() + 32); - in.push_back({ "okb.sig", signature.data(), signature.size(), 0 }); + in.push_back({ "okb.sig", signature.data(), signature.size(), 0, 0, 0, 0 }); + } + // COMPRESSED (0.4): each image a DEFLATE stream, which is not mapped and so + // not aligned; a runner expands it once, holds it to its digest, and maps + // what it expanded. + std::vector> deflated; + deflated.reserve(members.size()); + for (const auto& [k, m] : members) { + if (!compress) { in.push_back({ k.c_str(), m.bytes.data(), m.bytes.size(), 1, 0, 0, 0 }); continue; } + z_stream z{}; + if (deflateInit2(&z, 9, Z_DEFLATED, -15, 9, Z_DEFAULT_STRATEGY) != Z_OK) { std::println(std::cerr, "okld: the compressor cannot start"); return 1; } + std::vector d(deflateBound(&z, static_cast(m.bytes.size()))); + z.next_in = const_cast(m.bytes.data()); + z.avail_in = static_cast(m.bytes.size()); + z.next_out = d.data(); + z.avail_out = static_cast(d.size()); + const int r = deflate(&z, Z_FINISH); + d.resize(z.total_out); + deflateEnd(&z); + if (r != Z_STREAM_END) { std::println(std::cerr, "okld: {} could not be compressed", k); return 1; } + deflated.push_back(std::move(d)); + const auto& c = deflated.back(); + in.push_back({ k.c_str(), c.data(), c.size(), 0, 8, m.bytes.size(), okld_crc32(0, m.bytes.data(), m.bytes.size()) }); } - for (const auto& [k, m] : members) in.push_back({ k.c_str(), m.bytes.data(), m.bytes.size(), 1 }); const okld_u64 need = okld_zip_write(in.data(), in.size(), okld::okb_alignment, nullptr, 0); std::vector archive(need); okld_zip_write(in.data(), in.size(), okld::okb_alignment, archive.data(), archive.size()); if (!write_file(out, archive)) { std::println(std::cerr, "okld: {} cannot be written", out); return 1; } - std::println("{}: okb {}, {}, {} architecture(s), {} image(s){}, {} bytes", out, okld::okb_version, name, - archs.size(), members.size(), seed ? ", signed" : "", archive.size()); + std::println("{}: okb {}, {}, {} architecture(s), {} image(s){}{}, {} bytes", out, okb, name, + archs.size(), members.size(), seed ? ", signed" : "", compress ? ", compressed" : "", archive.size()); return 0; } @@ -427,9 +458,12 @@ int bundle_check(const std::vector& args) { const okld_zip_member* json = nullptr; const okld_zip_member* sig = nullptr; for (const auto& m : list) { - if (member_name(m) == "okb.json") json = &m; - if (member_name(m) == "okb.sig") sig = &m; - if (m.method != 0 || m.size != m.original) fail(std::format("{}: not stored", member_name(m))); + const auto n = member_name(m); + if (n == "okb.json") json = &m; + if (n == "okb.sig") sig = &m; + const bool image = okld_okb_is_image(n.data(), n.size()); + if (!(m.method == 0 && m.size == m.original) && !(image && m.method == 8)) + fail(std::format("{}: neither stored nor an image compressed (DEFLATE)", n)); } if (!json) { fail("no okb.json"); return 1; } if (okld_okb_admit(a.data(), a.size(), json, nullptr, 0, &why) != 0) { fail(why.text); return 1; } @@ -468,10 +502,21 @@ int bundle_check(const std::vector& args) { // needs it. std::map> by_dir; std::set refused; + // A compressed image's bytes, expanded and held to its digest (0.4). + std::map> expanded; + auto image_of = [&](const okld_zip_member* m) -> std::span { + if (auto e = expanded.find(m); e != expanded.end()) return e->second; + return std::span(a.data() + m->data, m->size); + }; for (const auto& m : list) { const auto n = member_name(m); if (n == "okb.json" || n == "okb.sig" || n.ends_with("/")) continue; - if (okld_okb_admit(a.data(), a.size(), &m, text, json->size, &why) != 0) { fail(why.text); refused.insert(&m); } + if (m.method == 8) { + std::vector e(m.original); + if (okld_okb_expand(a.data(), a.size(), &m, text, json->size, e.data(), e.size(), &why) != 0) { fail(why.text); refused.insert(&m); } + else expanded[&m] = std::move(e); + } + else if (okld_okb_admit(a.data(), a.size(), &m, text, json->size, &why) != 0) { fail(why.text); refused.insert(&m); } // The chunks a runner verifies instead, where they are stated: both // digests hold, or the okb is refused (docs/okb.md, 0.1.8). else if (okld_okb_admit_chunked(a.data(), a.size(), &m, text, json->size, nullptr, nullptr, &why) != 0) { @@ -490,25 +535,30 @@ int bundle_check(const std::vector& args) { const okld_okb_arch* arch = nullptr; for (const auto& x : std::span(manifest.archs, manifest.arch_count)) if (dir == x.dir) arch = &x; g_elsewhere.clear(); - for (const auto* m : images) - okld_exports(a.data() + m->data, m->size, [](const char* n, void*) { g_elsewhere.insert(n); }, nullptr); + for (const auto* m : images) { + if (refused.contains(m)) continue; + const auto b = image_of(m); + okld_exports(b.data(), b.size(), [](const char* n, void*) { g_elsewhere.insert(n); }, nullptr); + } for (const auto* m : images) { if (refused.contains(m)) continue; const auto n = member_name(*m); - const auto* image = a.data() + m->data; + const auto bytes = image_of(m); + const auto* image = bytes.data(); + const okld_size image_size = bytes.size(); okld_info info{}; - if (okld_read(image, m->size, &info, &why) != 0) { fail(std::format("{}: {}", n, why.text)); continue; } + if (okld_read(image, image_size, &info, &why) != 0) { fail(std::format("{}: {}", n, why.text)); continue; } const char* machine = okld_okb_arch_of(info.machine); if (!arch) fail(std::format("{}: in {}/, which okb.json names no architecture for", n, dir)); else if (!machine || std::string_view(machine) != arch->name) fail(std::format("{}: an image of {}, in the directory of {}", n, machine ? machine : "another architecture", arch->name)); unsigned char key[32]; - const int v = okld_verify(image, m->size, key); + const int v = okld_verify(image, image_size, key); if (v < 0) fail(std::format("{}: its signature does not hold", n)); else if (v == 0 && require) fail(std::format("{}: not signed, and a signature is required", n)); else if (v == 1 && !is_trusted(key)) fail(std::format("{}: signed by {}, which is not trusted", n, hex(key, 32))); struct ctx { std::string* name; int* failures; const std::string* path; } c{ const_cast(&n), &failures, &path }; - okld_check(image, m->size, + okld_check(image, image_size, [](const char* x, void*) { return surface().contains(x) ? 1 : 0; }, [](const char* x, void*) { return g_elsewhere.contains(x) ? 1 : 0; }, [](const char* sentence, void* p) { @@ -516,7 +566,7 @@ int bundle_check(const std::vector& args) { std::println(std::cerr, "okld: {}: {}: {}", *c.path, *c.name, sentence); ++*c.failures; }, &c); - okld_check_placement(image, m->size, + okld_check_placement(image, image_size, [](const char* sentence, void* p) { auto& c = *static_cast(p); std::println(std::cerr, "okld: {}: {}: {}", *c.path, *c.name, sentence); diff --git a/tools/src/okrun.cpp b/tools/src/okrun.cpp index b5c887f..8c62a9d 100644 --- a/tools/src/okrun.cpp +++ b/tools/src/okrun.cpp @@ -9,6 +9,10 @@ // separated); the report goes to /okprof-.json, // which `okprof report' reads (OKRUN_PROF says the same) // +// `okrun cache [list]' lists what the runner keeps for this machine (0.4: an +// image's data for this boot's base, snapshots, their markers); `okrun cache gc' +// removes what was prepared for another boot, and `--all' everything. +// // The image is told its arguments are the image's (or the okb's) name and what // follows it. The images it needs are found beside it, in `lib//' and // `lib/' there, and in OKRUN_PATH. @@ -18,11 +22,43 @@ namespace { unsigned long length(const char* s) { unsigned long n = 0; while (s && s[n]) ++n; return n; } void say(const char* s) { kal_stream_write(kal_stderr(), s, length(s)); } +void put(const char* s) { kal_stream_write(kal_stdout(), s, length(s)); } bool same(const char* a, const char* b) { for (;; ++a, ++b) { if (*a != *b) return false; if (!*a) return true; } } int hex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; } } // namespace +// `okrun cache [gc [--all]]': what the runner keeps for this machine (0.4). +int cache(int argc, char** argv) { + auto number = [](unsigned long long v) { + char d[24]; int n = 0; + do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + char out[24]; int k = 0; + while (n) out[k++] = d[--n]; + out[k] = 0; + say(out); + }; + if (argc >= 3 && same(argv[2], "gc")) { + const bool all = argc >= 4 && same(argv[3], "--all"); + const unsigned long long bytes = okrun_cache_reclaim(all ? 1 : 0); + say("okrun: removed "); number(bytes); say(all ? " bytes, everything the runner keeps\n" : " bytes prepared for another boot\n"); + return 0; + } + if (argc >= 3 && !same(argv[2], "list")) { say("usage: okrun cache [list | gc [--all]]\n"); return 2; } + const unsigned long long bytes = okrun_cache_list([](const char* name, unsigned long long size, int current, void*) { + unsigned long long v = size; + char d[24]; int n = 0; + do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + char out[24]; int k = 0; + while (n) out[k++] = d[--n]; + out[k] = 0; + put(out); put("\t"); put(current ? "this boot" : "another boot"); put("\t"); put(name); put("\n"); + }, nullptr); + say("okrun: "); number(bytes); say(" bytes\n"); + return 0; +} + int main(int argc, char** argv) { + if (argc >= 2 && same(argv[1], "cache")) return cache(argc, argv); okrun_options options{}; static unsigned char keys[16 * 32]; int i = 1; From 2f71cb87a6ddbba036c0904f999425037a72b3e5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 08:50:58 +0900 Subject: [PATCH 8/9] wip: the later okb version in the test is 3 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- ld/tests/okb_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ld/tests/okb_test.cpp b/ld/tests/okb_test.cpp index e65958b..f8e4b32 100644 --- a/ld/tests/okb_test.cpp +++ b/ld/tests/okb_test.cpp @@ -247,7 +247,7 @@ int main() { cut.resize(cut.size() - 30); check(okld_zip_each(cut.data(), cut.size(), nullptr, nullptr, &why) < 0, "a truncated archive is refused"); } - const char* later = "{\"okb\": 2, \"architectures\": {\"x86_64\": {}}, \"entry\": \"a.oke\"}"; + const char* later = "{\"okb\": 3, \"architectures\": {\"x86_64\": {}}, \"entry\": \"a.oke\"}"; check(okld_okb_read_manifest(later, std::strlen(later), &man, &why) < 0 && std::strstr(why.text, "later version"), "a later okb version is refused"); check(okld_okb_read_manifest("{\"okb\": 1, \"entry\": ", 20, &man, &why) < 0, "a truncated manifest is refused"); From 9d68ef58b81584cb34c049a9543e5d55296b6cd6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 3 Nov 2026 08:54:32 +0900 Subject: [PATCH 9/9] wip: why a snapshot was not used is in the report; CI prints it Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +++- run/src/run.cpp | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e10b19..5f3413b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -326,7 +326,9 @@ jobs: grep -q '^made' "$OKRUN_CACHE"/*.sealing || { echo "::error::no snapshot was made"; exit 1; } grep -q 'refused: an initialiser asked for kal_time_monotonic' "$OKRUN_CACHE"/*.sealing \ || { echo "::error::the initialiser that asked for the time was not refused"; exit 1; } - out=$(run snapshot); echo "$out" + rm -rf snapprof && mkdir snapprof + out=$(OKRUN_PROF=phases:snapprof run snapshot); echo "$out" + cat snapprof/*.json | tr -d '\r' | sed -n '/"runner"/,/]/p' expect "$out" 'started from the snapshot' 'the second start did not start from the snapshot' expect "$out" 'the memory is filled' 'the snapshot holds other memory' out=$(OKIMG_SNAPSHOT_NAME=set run snapshot) diff --git a/run/src/run.cpp b/run/src/run.cpp index 0335dec..e078f7a 100644 --- a/run/src/run.cpp +++ b/run/src/run.cpp @@ -1136,11 +1136,15 @@ constexpr char kSnapshotStart[] = "__okrun_snapshot_start"; // The snapshot of this boot's base, mapped into `s.prepared', when one was made // under this implementation's facts. bool snapshot_found(search& s, const local_place& lp, okld_prepared_info& info) { - auto made_here = [](const okld_prepared_info& i, const local_place&) { - return (i.flags & OKLD_PREPARED_SNAPSHOT) != 0 && i.facts == environment_facts(i.asked) && i.start != 0 - && i.state != 0; + auto a_snapshot = [](const okld_prepared_info& i, const local_place&) { + return (i.flags & OKLD_PREPARED_SNAPSHOT) != 0 && i.start != 0 && i.state != 0; }; - if (!map_prepared(lp, ".oksnap", s.prepared, info, made_here)) return false; + if (!map_prepared(lp, ".oksnap", s.prepared, info, a_snapshot)) return false; + if (info.facts != environment_facts(info.asked)) { + release_file(s.prepared); + profile_note("snapshot", "declined: the implementation's facts, or this start's answers, differ", 0); + return false; + } if (info.kept_size && !seal_kept_holds(s.prepared.data + info.kept, info.kept_size)) { release_file(s.prepared); profile_note("snapshot", "declined: a named value the initialisers read differs", 0); @@ -1557,7 +1561,13 @@ int enter(search& s, const void* bytes, unsigned long long size, const char* nam if (local && snapshot == snapshotting::use && snapshot_found(s, lp, snap)) { loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason); if (loaded == 0) from_snapshot = true; - else { release_file(s.prepared); reason = okld_why{}; } + else { + static char why_not[sizeof reason.text + 32]; + text(why_not, sizeof why_not) << "not loaded: " << reason.text; + profile_note("snapshot", why_not, 0); + release_file(s.prepared); + reason = okld_why{}; + } } if (loaded != 0 && local && prepare_local(s, bytes, size, lp)) { loaded = okld_load_prepared(bytes, size, name, &s.host, s.prepared.data, s.prepared.size, &m, &reason);