diff --git a/docs/abi-policy.md b/docs/abi-policy.md index 540bf1d03..edc84e41d 100644 --- a/docs/abi-policy.md +++ b/docs/abi-policy.md @@ -47,7 +47,10 @@ library). A newer minor whose duties it knows is read. **The runtime ABI.** `AETHER_RUNTIME_ABI_ID` (`runtime/aether_abi.h`) is the Aether version, `/abi` and `AETHER_RUNTIME_ABI_REV`, and the flags that change layouts (`+debug-mailbox`). A self-contained library carries -its own runtime, so this ABI never crosses its edge. A `--shared-runtime` +its own runtime, so this ABI never crosses its edge: on ELF `ae` links it +`-Bsymbolic`, so its runtime calls bind to its own copy, not to the one a +program linked `-rdynamic` exports, as macOS and Windows keep them apart by +themselves (#3481). A `--shared-runtime` library, and C emitted with `--emit=csrc` or `--emit=obj`, must run on the runtime it was made for. The shared runtime reports its ID (`aether_runtime_abi()`), and the catalog of a shared-runtime library @@ -87,7 +90,7 @@ catalogs and libraries compiled for a runtime of another ABI. | Combination | Supported | |---|---| | A C host and a self-contained library of any version | Yes, using the functions of the library's level (read `host_abi`); a host built with older headers uses its own level's | -| An Aether program importing a self-contained library | Yes, when the importer knows every duty the catalog requires; a library of an older version is read by its minor | +| An Aether program importing a self-contained library | Yes, when the importer knows every duty the catalog requires; a library of an older version is read by its minor. On ELF, a library of another runtime ABI linked before #3481 (without `-Bsymbolic`, so it would run on the program's runtime) is refused at import, naming the rebuild | | An importer older than schema 1.11 and a newer library | Calls that succeed work; it cannot read `requires`, so it does not check outcomes, and a failed call reads as its zero value, as with 0.856's own libraries | | Anything on `--shared-runtime` | Only on the runtime it was made for (one runtime ABI ID) | | Two self-contained libraries of different versions in one process | Yes; each runs on its own runtime, and the shared facilities are refused across layouts | diff --git a/docs/design/ownership-model.md b/docs/design/ownership-model.md index bdc3b3011..b47870531 100644 --- a/docs/design/ownership-model.md +++ b/docs/design/ownership-model.md @@ -901,10 +901,11 @@ struct holding a string has no such copy: by value, the program's drop releases the library's strings, and through a typed pointer the program's stores do. A builder that uses its config fills, grows or frees a map the program made, and a factory in the library would not help, since the -trailing block fills the config with the program's runtime. So where the -copies stay apart (macOS, Windows: no ELF interposition) `ae` refuses to -import a static-runtime library whose exports pass such a struct or whose -builders use their config, and names the rebuild with `--shared-runtime`. +trailing block fills the config with the program's runtime. So, since the +copies stay apart on every platform (#3481: a self-contained library binds +to its own runtime on ELF too), `ae` refuses to import a static-runtime +library whose exports pass such a struct or whose builders use their +config, and names the rebuild with `--shared-runtime`. Closures cross either way: an environment carries its own release, which runs in the library that made it. diff --git a/docs/emit-lib.md b/docs/emit-lib.md index b9ea3b2d7..28abff5e0 100644 --- a/docs/emit-lib.md +++ b/docs/emit-lib.md @@ -1254,11 +1254,13 @@ Scope of the current implementation: A library built the default way carries its own copy of the runtime, linked in statically: one scheduler, one set of panic frames, one config per image. That keeps a library self-contained for a C or Python host, but a program -that imports Aether libraries in binary form then runs on several runtimes. -On Linux and FreeBSD ELF symbol interposition happens to bind a library's -runtime calls to the program's copy; macOS's two-level namespace and Windows' -PE keep them apart, so a panic raised inside a library never reaches the -program's `catch` there. +that imports Aether libraries in binary form then runs on several runtimes, +on every platform: a panic raised inside a library never reaches the +program's `catch`. macOS's two-level namespace and Windows' PE keep the +copies apart by themselves; on ELF (Linux, FreeBSD) `ae` links a +self-contained library with `-Bsymbolic`, so its runtime calls bind to its +own copy, not to the one a program linked `-rdynamic` exports (#3481: they +did, and a library made for one release then ran on another's runtime). `--shared-runtime` links against the runtime as a shared library instead, `aether.dll` (with `libaether.dll.a`), `libaether.dylib` or `libaether.so`, @@ -1324,10 +1326,9 @@ ae build app.ae -o app # has none way out, `main()` drains the scheduler and joins it, so a message the library sent fire-and-forget is still delivered. - **Draining needs one runtime.** The drain reaches the library's actors - where the program and the library share one runtime: `--shared-runtime` - everywhere, or ELF interposition with the default static runtime (Linux, - FreeBSD). On macOS and Windows a static library's actors run on that - library's own scheduler. `ae` warns that messages still in flight when the + where the program and the library share one runtime: `--shared-runtime`. + A static library's actors run on that library's own scheduler, on every + platform (#3481). `ae` warns that messages still in flight when the program exits are not delivered, and names the library to rebuild with `--shared-runtime`. - **A C host** calls `scheduler_shutdown()` before it exits to deliver what diff --git a/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md new file mode 100644 index 000000000..2b653af0b --- /dev/null +++ b/new_changelogs/20261030T170000Z-fixed-3481-library-keeps-its-runtime-on-elf.md @@ -0,0 +1,15 @@ +- **A library with a runtime of its own keeps it on Linux and FreeBSD + (#3481).** An `ae build` program is linked `-rdynamic`, so it exports + its runtime, and on ELF a library's calls into the runtime it carries + bound to the program's instead: its code ran on a runtime it was not + made for. Across versions its runtime ABI check refused, and the program + ended on the panic ("the library was made for Aether runtime 0.860.0/abi1 + and runs on 0.863.0/abi1"), which kept the cross-version ABI matrix red + on Linux since 0.861.0. `ae` now links such a library `-Bsymbolic`, as + macOS and Windows keep the copies apart by themselves, so on every + platform a static-runtime library runs on its own runtime: `ae` refuses + an import that needs one runtime (a struct holding strings, a builder + using its config) and warns that a library's actors are not drained at + exit, naming `--shared-runtime`, as it did on macOS and Windows. A + library of another runtime ABI linked before this is refused at import + on ELF, naming the rebuild, instead of ending the program. diff --git a/tests/integration/abi_matrix/test_abi_matrix.sh b/tests/integration/abi_matrix/test_abi_matrix.sh index 6a7e956c6..e38806574 100644 --- a/tests/integration/abi_matrix/test_abi_matrix.sh +++ b/tests/integration/abi_matrix/test_abi_matrix.sh @@ -13,7 +13,10 @@ # a panic in one, an account of the new one suspended while the old one # runs and resumed, both unloaded # Aether consumers (app.ae, a binary import): -# this importer, the old library; the old importer, this library +# this importer, the old library; the old importer, this library. On +# ELF this ae links a library -Bsymbolic, so it keeps its own runtime in +# a program that exports one (#3481); an old library linked without it, +# of another version, is refused at import, naming the rebuild. # The shared runtime: # this host, this library on this shared runtime the baseline # this host, the old library on the old shared runtime @@ -159,10 +162,47 @@ run_app() { bad "$1: app.ae did not build" "$2/app_build.log" return fi - expect "$1" "$(cd "$2" && PATH="$2:$PATH" LD_LIBRARY_PATH="$2${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ - DYLD_LIBRARY_PATH="$2${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ./app$exe 2>"$2/app.err" | tr -d '\r')" "$app_want" + got="$(cd "$2" && PATH="$2:$PATH" LD_LIBRARY_PATH="$2${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + DYLD_LIBRARY_PATH="$2${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" ./app$exe 2>"$2/app.err" | tr -d '\r')" + expect "$1" "$got" "$app_want" + # What the program said on stderr: an abort prints nothing on stdout. + if [ "$got" != "$app_want" ] && [ -s "$2/app.err" ]; then + echo " stderr:"; tail -12 "$2/app.err" | sed 's/^/ /' + fi +} +# A library with a runtime of its own linked before #3481 on ELF binds that +# runtime to the program's (an `ae build` program exports its runtime), so it +# would run on this one, of another version: this importer refuses it at +# build, naming the rebuild, instead of the program ending on its first call. +refuse_app() { + cp "$SCRIPT_DIR/app.ae" "$2/app.ae" + if (cd "$2" && AETHER_HOME="$3" "$4" build app.ae -o app > app_build.log 2>&1); then + bad "$1: app.ae built; the import was not refused" "$2/app_build.log" + elif ! grep -q "carries the Aether runtime" "$2/app_build.log" || + ! grep -q -- "--shared-runtime" "$2/app_build.log"; then + bad "$1: the refusal does not say why and name the rebuild" "$2/app_build.log" + fi } -run_app "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE" +this_version="$(tr -d '\r\n' < "$ROOT/VERSION" 2>/dev/null)" +# This ae links a library with a runtime of its own -Bsymbolic on ELF, so it +# keeps that runtime in any program (#3481). +if [ "$ext" = so ] && command -v readelf >/dev/null 2>&1 && + ! readelf -d "$TMP/new/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then + bad "this ae linked libmlib.$ext without -Bsymbolic: it would run on an importing program's runtime" +fi +old_binds_own=1 +if [ "$ext" = so ] && [ "$old_version" != "$this_version" ]; then + if ! command -v readelf >/dev/null 2>&1; then + old_binds_own=unknown + elif ! readelf -d "$TMP/old/libmlib.$ext" 2>/dev/null | grep -q SYMBOLIC; then + old_binds_own=0 + fi +fi +case "$old_binds_own" in + 0) refuse_app "this importer, the $old_version library (linked before #3481)" "$TMP/old" "$ROOT" "$AE" ;; + unknown) echo " [NOTE] abi_matrix: no readelf, so this importer with the $old_version library is not run" ;; + *) run_app "this importer, the $old_version library" "$TMP/old" "$ROOT" "$AE" ;; +esac run_app "the $old_version importer, this library" "$TMP/new" "$OLD_HOME" "$OLD_AE" # The shared runtime. $1 = case dir, $2 = library dir, $3 = runtime dir, diff --git a/tests/integration/binary_import_actors/test_binary_import_actors.sh b/tests/integration/binary_import_actors/test_binary_import_actors.sh index 8aa1134ec..ecb0666d1 100644 --- a/tests/integration/binary_import_actors/test_binary_import_actors.sh +++ b/tests/integration/binary_import_actors/test_binary_import_actors.sh @@ -11,10 +11,9 @@ # way out: crowd's slow fire-and-forget message is still delivered. # # Two passes over the same sources: -# static runtime each library carries its own libaether. The actors run -# wherever the importer's calls land; the exit drain reaches -# them only where ELF interposition merges the copies -# (Linux, FreeBSD). Elsewhere `ae` warns that it cannot. +# static runtime each library carries its own libaether and runs its +# actors on its own scheduler, which the program's exit +# does not drain, on every platform (#3481); `ae` warns. # shared runtime `--shared-runtime`: one runtime, one scheduler, and the # program's exit drains it on every platform. set -e @@ -25,10 +24,9 @@ AE="$ROOT/build/ae" [ -x "$AE" ] || [ -x "$AE.exe" ] || { echo " [SKIP] binary_import_actors: ae not built"; exit 0; } case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac fail() { @@ -65,7 +63,7 @@ run_pass() { echo "$OUT" | grep -q "^OK" || { echo "$OUT"; fail "$label: the actors did not count to 300"; } if [ "$drains" = 1 ]; then echo "$OUT" | grep -q "^late 7" || { echo "$OUT"; fail "$label: the exit did not drain the library's in-flight message" run.log; } - elif [ "$ELF" = 0 ]; then + else grep -q "runs actors on its own static runtime" run.log \ || fail "$label: no warning that the static runtime's messages are not drained" run.log fi @@ -80,7 +78,7 @@ run_pass() { } # ---- static runtime ---- -run_pass "$WORK/static" "" "static runtime" "$ELF" +run_pass "$WORK/static" "" "static runtime" 0 # ---- shared runtime (where this toolchain built one) ---- if [ -d "$ROOT/build/shared" ]; then diff --git a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh index 13500f894..990254191 100644 --- a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh +++ b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh @@ -11,9 +11,9 @@ # finds nothing on macOS. # 3. A struct holding a string, returned by value, and a builder that frees # its config. Neither can be copied between two copies of the runtime, so -# they need one: the shared runtime, or the static one where ELF -# interposition merges the copies (Linux, FreeBSD). There they run and -# grow 0; elsewhere the import is refused, naming the rebuild. +# they need one, the shared runtime: with the static one each library +# keeps its own copy (on every platform since #3481), and the import is +# refused, naming the rebuild. On the shared runtime they run and grow 0. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" @@ -22,10 +22,9 @@ AE="$ROOT/build/ae" CC="${CC:-cc}" case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac LEAKS=0 if [ "$(uname -s)" = Darwin ] && command -v leaks >/dev/null 2>&1; then LEAKS=1; fi @@ -102,7 +101,7 @@ run_pass() { cd "$WORK" } -run_pass "$WORK/static" "" "static runtime" "$ELF" +run_pass "$WORK/static" "" "static runtime" 0 # The C host, against the static library. cd "$WORK/static" diff --git a/tests/integration/binary_import_structs/app_panic.ae b/tests/integration/binary_import_structs/app_panic.ae index 4b9c8d614..d0e8f6eff 100644 --- a/tests/integration/binary_import_structs/app_panic.ae +++ b/tests/integration/binary_import_structs/app_panic.ae @@ -1,8 +1,8 @@ // A panic inside a binary-imported library reaches the host's catch -// (#2297). Holds where the host and the library share one runtime: on ELF -// (Linux, FreeBSD) the library's runtime symbols bind to the host's by -// interposition. The test script runs this only there; macOS and Windows -// keep a runtime per image until #2297's shared-runtime work lands. +// (#2297). Holds where the host and the library share one runtime: a +// library built with --shared-runtime, which is how the test script runs +// it. A library with a runtime of its own keeps it on every platform +// (#3481), so its panic stays in it. import engine diff --git a/tests/integration/binary_import_structs/test_binary_import_structs.sh b/tests/integration/binary_import_structs/test_binary_import_structs.sh index e082d45ea..55394234d 100644 --- a/tests/integration/binary_import_structs/test_binary_import_structs.sh +++ b/tests/integration/binary_import_structs/test_binary_import_structs.sh @@ -10,14 +10,11 @@ # and a function taking or returning a struct was not exported at all. # # Two passes over the same sources: -# static runtime each library carries its own copy of libaether. Where -# ELF interposition merges the copies (Linux, FreeBSD), a -# second binary library (script) importing the engine, and -# the host importing both, share the engine's one module -# state, and a panic in the engine reaches the host's -# catch. Elsewhere the copies stay apart and neither may -# free the other's strings, so importing the engine, whose -# *Model holds one, is refused, naming the rebuild (#2931). +# static runtime each library carries its own copy of libaether, and on +# every platform (#3481) the copies stay apart: neither +# may free the other's strings, so importing the engine, +# whose *Model holds one, is refused, naming the rebuild +# (#2931). # shared runtime `--shared-runtime`: the libraries link aether.dll / # libaether.so / libaether.dylib, their catalogs say so # (schema 1.5), and the host follows. One runtime, so the @@ -30,10 +27,9 @@ AE="$ROOT/build/ae" [ -x "$AE" ] || [ -x "$AE.exe" ] || { echo " [SKIP] binary_import_structs: ae not built"; exit 0; } case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; ELF=0; EXE=".exe" ;; - Darwin) SO_EXT=".dylib"; ELF=0; EXE="" ;; - Linux|FreeBSD) SO_EXT=".so"; ELF=1; EXE="" ;; - *) SO_EXT=".so"; ELF=0; EXE="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) SO_EXT=".dll"; EXE=".exe" ;; + Darwin) SO_EXT=".dylib"; EXE="" ;; + *) SO_EXT=".so"; EXE="" ;; esac fail() { @@ -88,25 +84,20 @@ panic_check() { } # ---- static runtime ---- -if [ "$ELF" = 1 ]; then - run_pass "$WORK/static" "" "static runtime" - panic_check "$WORK/static" "static runtime" -else - mkdir -p "$WORK/static" - cp "$SCRIPT_DIR/engine.ae" "$SCRIPT_DIR/app.ae" "$WORK/static/" - cd "$WORK/static" - AETHER_HOME="$ROOT" "$AE" build --emit=lib engine.ae -o "libengine$SO_EXT" >engine.log 2>&1 \ - || fail "static runtime: ae build --emit=lib engine.ae" engine.log - rm -f engine.ae - if AETHER_HOME="$ROOT" "$AE" run app.ae >run.log 2>&1; then - fail "static runtime: importing a library whose *Model holds a string was not refused" run.log - fi - grep -q "model_new\` passes \*Model, which holds strings" run.log \ - || fail "static runtime: the refusal does not name model_new and *Model" run.log - grep -q "ae build --emit=lib --shared-runtime" run.log \ - || fail "static runtime: the refusal does not name the rebuild" run.log - cd "$WORK" +mkdir -p "$WORK/static" +cp "$SCRIPT_DIR/engine.ae" "$SCRIPT_DIR/app.ae" "$WORK/static/" +cd "$WORK/static" +AETHER_HOME="$ROOT" "$AE" build --emit=lib engine.ae -o "libengine$SO_EXT" >engine.log 2>&1 \ + || fail "static runtime: ae build --emit=lib engine.ae" engine.log +rm -f engine.ae +if AETHER_HOME="$ROOT" "$AE" run app.ae >run.log 2>&1; then + fail "static runtime: importing a library whose *Model holds a string was not refused" run.log fi +grep -q "model_new\` passes \*Model, which holds strings" run.log \ + || fail "static runtime: the refusal does not name model_new and *Model" run.log +grep -q "ae build --emit=lib --shared-runtime" run.log \ + || fail "static runtime: the refusal does not name the rebuild" run.log +cd "$WORK" # ---- shared runtime (where this toolchain built one) ---- if [ -d "$ROOT/build/shared" ]; then diff --git a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh index e81c0bfda..ef5d426bb 100644 --- a/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh +++ b/tests/integration/sandbox_nested_libraries/test_sandbox_nested_libraries.sh @@ -12,10 +12,9 @@ # # 1. one runtime: both libraries' C linked into the host; # 2. each library built the default way, with its own copy of the runtime: -# on macOS and Windows the copies stay apart, so the inner one cannot read -# the outer block's grants; it refuses, and its message names -# --shared-runtime (on Linux and FreeBSD ELF interposition binds both to -# one copy: the same answers, no message); +# the copies stay apart (on every platform since #3481), so the inner one +# cannot read the outer block's grants; it refuses, and its message names +# --shared-runtime; # 2c. the outer library linked into a program with a runtime of its own # that exports nothing and looks first (#3445); # 2d. a program with the panic and no sandbox, -rdynamic on Linux, and a @@ -37,10 +36,10 @@ fail=0 want="before 1 inner 0 granted 0 outer 0 after 1" case "$(uname -s 2>/dev/null)" in - MINGW*|MSYS*|CYGWIN*|Windows_NT) ext=dll; apart=1; dl="" ;; - Darwin) ext=dylib; apart=1; dl="" ;; - Linux) ext=so; apart=0; dl="-ldl" ;; - *) ext=so; apart=0; dl="" ;; + MINGW*|MSYS*|CYGWIN*|Windows_NT) ext=dll; dl="" ;; + Darwin) ext=dylib; dl="" ;; + Linux) ext=so; dl="-ldl" ;; + *) ext=so; dl="" ;; esac # 1. one runtime @@ -95,7 +94,7 @@ if [ "$got" != "$want" ]; then head -5 "$TMP/apart.err" | sed 's/^/ /' fail=1 fi -if [ "$apart" = 1 ] && ! grep -q -- "--shared-runtime" "$TMP/apart.err"; then +if ! grep -q -- "--shared-runtime" "$TMP/apart.err"; then echo " [FAIL] sandbox_nested_libraries: a runtime per library: the refusal does not name --shared-runtime" head -5 "$TMP/apart.err" | sed 's/^/ /' fail=1 @@ -136,10 +135,11 @@ fi # 2d. a program with the panic of a runtime of its own and no sandbox, # linked -rdynamic on Linux as `ae build` links one, loading a library with a # runtime of its own whose enforce block catches a panic raised in a nested -# block (#3438): under ELF interposition the library runs the program's panic -# and try frames, so its sandbox must set the program's pointers. With them -# each copy's own, the catch left the nested level in force (PATH 0) and the -# block's end the outer one (probe 0 after it). +# block (#3438). Before #3481 the library ran the program's panic and try +# frames there (ELF interposition), so its sandbox had to set the program's +# pointers; now it runs its own on every platform, and the catch must still +# leave neither the nested level (PATH 0) nor, at the block's end, the outer +# one (probe 0 after it) in force. rdyn="" case "$(uname -s 2>/dev/null)" in Linux|FreeBSD) rdyn="-rdynamic" ;; esac # shellcheck disable=SC2046,SC2086 @@ -198,6 +198,6 @@ else fi if [ "$fail" = 0 ]; then - echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused where the copies stay apart), a program whose copy looked first and exports nothing, a panic caught inside the block of a library that runs the program's panic$shared_note" + echo " [PASS] sandbox_nested_libraries: an inner library's export inside an outer enforce is held to it, its own grant_all narrows nothing, and the outer block stays checked: one runtime, a runtime per library (refused, naming --shared-runtime), a program whose copy looked first and exports nothing, a panic caught inside the block of a library loaded by a program linked -rdynamic$shared_note" fi exit $fail diff --git a/tools/ae.c b/tools/ae.c index 9d973b559..4bcb71e8a 100644 --- a/tools/ae.c +++ b/tools/ae.c @@ -498,6 +498,62 @@ static const char* const k_ae_import_duties[] = { #define AE_CATALOG_MINOR_KNOWN 11 +#if !defined(_WIN32) && !defined(__APPLE__) +/* An unsigned field of n bytes of an ELF file, in its byte order. */ +static uint64_t ae_elf_field(const unsigned char* p, int n, int big) { + uint64_t v = 0; + for (int i = 0; i < n; i++) v |= (uint64_t)p[big ? n - 1 - i : i] << (8 * i); + return v; +} + +/* #3481: 1 when the ELF library at `path` binds the definitions it holds to + * itself (DT_SYMBOLIC, or DF_SYMBOLIC in DT_FLAGS: linked -Bsymbolic, as + * `ae` links a library with a runtime of its own since #3481), 0 when it + * does not, -1 when it cannot be read. One that does not binds the runtime + * it carries to the program's when the program exports one (an `ae build` + * program is linked -rdynamic), and so runs on the program's runtime. Read + * from the file's headers and dynamic segment. */ +static int ae_elf_binds_own(const char* path) { + FILE* f = fopen(path, "rb"); + if (!f) return -1; + unsigned char eh[64]; + int r = -1; + size_t got = fread(eh, 1, sizeof(eh), f); + if (got >= 52 && memcmp(eh, "\x7f" "ELF", 4) == 0 && (eh[4] == 1 || eh[4] == 2)) { + int is64 = eh[4] == 2, big = eh[5] == 2; + uint64_t phoff = is64 ? ae_elf_field(eh + 32, 8, big) : ae_elf_field(eh + 28, 4, big); + int phentsize = (int)ae_elf_field(eh + (is64 ? 54 : 42), 2, big); + int phnum = (int)ae_elf_field(eh + (is64 ? 56 : 44), 2, big); + unsigned char ph[56]; + for (int i = 0; i < phnum && phentsize >= (is64 ? 56 : 32) && r < 0; i++) { + if (fseek(f, (long)(phoff + (uint64_t)i * (uint64_t)phentsize), SEEK_SET) != 0 || + fread(ph, 1, is64 ? 56 : 32, f) != (size_t)(is64 ? 56 : 32)) + break; + if (ae_elf_field(ph, 4, big) != 2) continue; /* PT_DYNAMIC */ + uint64_t off = is64 ? ae_elf_field(ph + 8, 8, big) : ae_elf_field(ph + 4, 4, big); + uint64_t size = is64 ? ae_elf_field(ph + 32, 8, big) : ae_elf_field(ph + 16, 4, big); + int ent = is64 ? 16 : 8; + if (size > (1u << 20) || fseek(f, (long)off, SEEK_SET) != 0) break; + unsigned char* dyn = (unsigned char*)malloc(size ? size : 1); + if (!dyn) break; + if (fread(dyn, 1, size, f) == size) { + r = 0; + for (uint64_t k = 0; k + (uint64_t)ent <= size; k += (uint64_t)ent) { + uint64_t tag = ae_elf_field(dyn + k, ent / 2, big); + uint64_t val = ae_elf_field(dyn + k + ent / 2, ent / 2, big); + if (tag == 0) break; /* DT_NULL */ + if (tag == 16) r = 1; /* DT_SYMBOLIC */ + if (tag == 30 && (val & 0x2)) r = 1; /* DT_FLAGS, DF_SYMBOLIC */ + } + } + free(dyn); + } + } + fclose(f); + return r; +} +#endif + /* A library's catalog against this importer (#2957): 0 when it can be used; * otherwise each reason on stderr, naming what is missing, both versions and * the fix, and -1. Called before anything of the library is used. */ @@ -543,6 +599,22 @@ static int ae_lib_check_compat(const _AeLibInfoMeta* m, const char* so_path) { so_path, m->runtime_abi ? m->runtime_abi : "(none)", AETHER_RUNTIME_ABI_ID); refused = 1; } +#if !defined(_WIN32) && !defined(__APPLE__) + /* #3481: a library with a runtime of its own, linked before `ae` bound + * one to itself on ELF, binds to the program's (an `ae build` program + * exports its runtime) and so runs on a runtime of another version than + * the one it was made for: its first call would refuse, and the program + * end on the panic. Refused here instead, naming the rebuild. */ + else if (!shared && minor >= 11 && m->runtime_abi && + strcmp(m->runtime_abi, AETHER_RUNTIME_ABI_ID) != 0 && ae_elf_binds_own(so_path) == 0) { + fprintf(stderr, "Error: %s carries the Aether runtime %s, but was linked by Aether %s to run on\n" + " the program's runtime instead, and this program's is %s.\n" + " Rebuild the library with this Aether (it then keeps its own runtime), or with\n" + " `ae build --emit=lib --shared-runtime`.\n", + so_path, m->runtime_abi, built_by, AETHER_RUNTIME_ABI_ID); + refused = 1; + } +#endif return refused ? -1 : 0; } @@ -5107,7 +5179,23 @@ void build_gcc_cmd(char* cmd, size_t size, * weak C main() for whoever links the OBJECT into an executable (see * emit_lib_weak_main in codegen.c); a shared library ae links itself has * no use for one, and should not export an entry point. */ + /* #3481: a library that carries its own runtime runs on it. On ELF a + * program linked -rdynamic exports its runtime, and the library's calls + * into its own copy bound to the program's instead (interposition): the + * library's code ran on a runtime it was not made for, of another + * version across releases, which its runtime ABI check refuses. + * -Bsymbolic binds a library's references to what it defines itself, + * as macOS's two-level namespace and Windows' PE always have; it still + * exports them, for a C host. A --shared-runtime library defines no + * runtime of its own, and keeps binding to the one it shares. */ +#if !defined(__APPLE__) + const char* emit_lib_flags = (g_emit_lib && !g_emit_exe) + ? (g_shared_runtime ? "-fPIC -shared -DAETHER_NO_LIB_MAIN " + : "-fPIC -shared -Wl,-Bsymbolic -DAETHER_NO_LIB_MAIN ") + : ""; +#else const char* emit_lib_flags = (g_emit_lib && !g_emit_exe) ? "-fPIC -shared -DAETHER_NO_LIB_MAIN " : ""; +#endif // Coverage builds skip -pipe — gcov works fine with it, but it // adds nothing when -O0 -g is already forced. Keeping the flag // string short helps the cmd-buffer size budget. @@ -5753,11 +5841,11 @@ static const char* ae_signature_string_struct(const _AeLibInfoMeta* m, int minor * values. A string crosses as a copy; a struct holding strings (by value or * through a typed pointer, where the program's stores release the * library's strings) and a builder's config the library uses have no copy, - * so where the library keeps its own runtime (no ELF interposition) such an - * import is refused, naming the rebuild that gives both one runtime. */ + * so a library that keeps its own runtime (every platform since #3481: on + * ELF it no longer binds to the program's) has such an import refused, + * naming the rebuild that gives both one runtime. */ static void ae_refuse_split_ownership(const _AeLibInfoMeta* m, int minor, const char* module, const char* so_path) { -#if defined(_WIN32) || defined(__APPLE__) char ty[200]; int refused = 0; for (int i = 0; i < m->function_count && m->functions; i++) { @@ -5799,9 +5887,6 @@ static void ae_refuse_split_ownership(const _AeLibInfoMeta* m, int minor, const "on the program's runtime.\n"); exit(1); } -#else - (void)m; (void)minor; (void)module; (void)so_path; -#endif } /* `module` NULL: the whole catalog (a bare import of a one-module @@ -6492,10 +6577,10 @@ static void prepare_binary_imports(const char* main_file) { g_binimport_static_names, g_binimport_static_rt > 1 ? "were" : "was", g_binimport_static_rt > 1 ? "them" : "it"); } -#if defined(_WIN32) || defined(__APPLE__) - /* Without ELF interposition, a static-runtime library's actors run on - * the library's own scheduler, which the program's exit does not drain: - * a message still in flight when main() returns is not delivered. */ + /* A static-runtime library's actors run on the library's own scheduler + * (on every platform since #3481), which the program's exit does not + * drain: a message still in flight when main() returns is not + * delivered. */ if (g_binimport_static_actor_names[0]) { fprintf(stderr, "Warning: %s runs actors on its own static runtime, so messages still in flight\n" @@ -6503,7 +6588,6 @@ static void prepare_binary_imports(const char* main_file) { " `ae build --emit=lib --shared-runtime` to run them on the program's.\n", g_binimport_static_actor_names); } -#endif } /* #2297: Windows has no rpath. After a link that names DLLs (binary imports,