. "$BST_LIB" # chrpath 0.16 (poky recipes-devtools/chrpath) -- the ELF RPATH/RUNPATH editor. # # NOTHING ON THIS IMAGE IS AN ELF FILE, AND THAT IS THE FACT THIS SUITE IS BUILT # AROUND. Every executable here is a WebAssembly module -- `\0asm', not `\177ELF' # -- so chrpath has no native subject on the target and case chrpath/self-not-elf # asserts exactly that, by pointing chrpath at its own installed binary. The # subject matter is therefore SHIPPED: four x86-64/i386 ELF fixtures in $BST_DATA, # which chrpath reads and rewrites as data. That kmod does the same thing with # foreign-architecture .ko files (93-kmod.test) is the precedent; parsing a # foreign ELF is chrpath's supported case, not a stretch, because the class and # the byte order are dispatched at RUN time from e_ident (elf.c:is_e32_flag, # swap_bytes_flag) rather than being compiled in. # # The fixtures ship xz-compressed for the same reason kmod's do: brintos-selftests # is an allarch recipe, and an uncompressed ELF in data/ is handed to a # sysroot_strip that has no llvm-strip to give it to. # # HOW THE FIXTURES WERE MADE (x86-64 host, gcc 9.5.0, GNU ld 2.38), so that any # value pinned below can be re-derived rather than trusted: # # libbstfix.c: int bst_answer(void) { return 4242; } # prog.c: prints "BST-CHRPATH-OK 4242" via bst_answer() # gcc -shared -fPIC -o lib-original-long-directory-name/libbstfix.so.1 libbstfix.c # cp lib-original-long-directory-name/libbstfix.so.1 lib/libbstfix.so.1 # gcc -o prog64-rpath prog.c -L lib-original-long-directory-name \ # -l:libbstfix.so.1 -Wl,-rpath,'$ORIGIN/lib-original-long-directory-name' \ # -Wl,--disable-new-dtags # --enable-new-dtags for the RUNPATH one # gcc -o prog64-nopath prog.c ... # no -rpath at all # gcc -m32 -o prog32-rpath ... # -Wl,-rpath,'$ORIGIN/lib32-original-long-dir' # # THE 40-CHARACTER RPATH IS LOAD-BEARING ON THE HOST, WHICH IS WHAT MAKES THE # TICKET'S "must still run" A MEASUREMENT RATHER THAN A HOPE. On x86-64 the # pristine fixture RUNS (it finds libbstfix.so.1 through the long directory) and # FAILS with "cannot open shared object file" once that directory is renamed -- # so the RPATH is genuinely the thing resolving the library. After # `chrpath -r '$ORIGIN/lib'` it runs again, now through the short directory, and # keeps running with the long directory deleted. That whole sequence is a HOST # measurement and is recorded in the ticket's design doc; what this suite pins on # TARGET is chrpath/rewrite-sha256 -- the target's rewritten bytes being identical # to the host's rewritten bytes, which is the only form the claim can take on a # machine with no ELF loader. See chrpath/rewritten-still-runs. require chrpath chrpath/present require xz chrpath/xz-present check_out chrpath/version "chrpath version 0.16" chrpath -v # Output contains a needle, whatever the exit status -- the error paths below # assert their status separately with check_rc, and check_out would fail them on # the status alone before ever looking at the message. check_msg() { _cm_id=$1; _cm_needle=$2; shift 2 _cm_out=$("$@" 2>&1) case "$_cm_out" in *"$_cm_needle"*) bst_ok "$_cm_id" ;; *) bst_notok "$_cm_id" "$(_bst_1line "$_cm_out")" ;; esac } sha() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1; } tmpd=${TMPDIR:-/tmp}/bst-chrpath.$$ mkdir -p "$tmpd" # The shipped payloads, asserted as files before anything is asked of them: a # `cp` of a path that does not exist would otherwise turn every case below into # the same uninformative red. for f in chrpath-rpath-x86_64 chrpath-runpath-x86_64 chrpath-nopath-x86_64 chrpath-rpath-i386; do if [ -f "$BST_DATA/$f.xz" ]; then bst_ok "chrpath/data-$f" else bst_notok "chrpath/data-$f" "missing $BST_DATA/$f.xz"; fi done xz -dc "$BST_DATA/chrpath-rpath-x86_64.xz" > "$tmpd/rpath64" 2>/dev/null xz -dc "$BST_DATA/chrpath-runpath-x86_64.xz" > "$tmpd/runpath64" 2>/dev/null xz -dc "$BST_DATA/chrpath-nopath-x86_64.xz" > "$tmpd/nopath64" 2>/dev/null xz -dc "$BST_DATA/chrpath-rpath-i386.xz" > "$tmpd/rpath32" 2>/dev/null # The decompression is itself an assertion: a truncated fixture would otherwise # reach chrpath as a short file and be reported as "isn't an ELF file", which is # the diagnosis of a DIFFERENT fault. check_eq chrpath/fixture-rpath64-sha \ ef1590a2d4e546637b11880e13c171f1977b3f4532d02d84fdcbfe549043cba1 "$(sha "$tmpd/rpath64")" check_eq chrpath/fixture-runpath64-sha \ 33e9194a7ed26160f9361ccfddba1e671c1a64c6ff19990ea60d6679a3af0e79 "$(sha "$tmpd/runpath64")" check_eq chrpath/fixture-nopath64-sha \ 0023a52a84b079ba6a17b76671a311b3d0d7c7c1b7e90736ec4045fd46a8e087 "$(sha "$tmpd/nopath64")" check_eq chrpath/fixture-rpath32-sha \ 6ff44ce1492bc73709864afe0433df4c06251f9d3f2110e52cc2c289e82051b2 "$(sha "$tmpd/rpath32")" # --------------------------------------------------------------------------- # The ticket's own criterion, in order: -l reports the RPATH of a staged binary, # -r rewrites it to a shorter value, and a re-read returns the new one. # --------------------------------------------------------------------------- cp "$tmpd/rpath64" "$tmpd/a" check_out chrpath/list-rpath 'RPATH=$ORIGIN/lib-original-long-directory-name' chrpath -l "$tmpd/a" check_out chrpath/replace-rpath 'new RPATH: $ORIGIN/lib' chrpath -r '$ORIGIN/lib' "$tmpd/a" check_out chrpath/reread-rpath 'RPATH=$ORIGIN/lib' chrpath -l "$tmpd/a" # ...and the re-read reports the NEW value only. The case above would pass on a # chrpath that had changed nothing, because the old string CONTAINS the new one # as a prefix; this is the case that makes the pair mean something. check_rc chrpath/reread-old-gone 1 \ sh -c "chrpath -l '$tmpd/a' 2>&1 | grep -q 'lib-original-long-directory-name'" # -r rewrites IN PLACE: the new string is written into the old string's slot in # .dynstr, zero-filled to the old length (chrpath.c: memset then strcpy), so the # file cannot change size. A size change would mean something other than the # documented edit happened. check_eq chrpath/rewrite-size-unchanged 16000 "$(wc -c < "$tmpd/a" | tr -d ' ')" # THE CROSS-MACHINE CLAIM. This digest was produced by the same operation on # x86-64, on a binary that was executed before and after the rewrite. Matching it # byte for byte is what carries the ticket's "the rewritten binary must still # run" onto a target that cannot run an ELF at all. check_eq chrpath/rewrite-sha256 \ 1b18c265efe8aa06c9a666f66f0539654aab19b0c555795ac655b153bf9e506c "$(sha "$tmpd/a")" # A self-referential oracle that needs no stored value: rewriting back to the # original 40-character string must restore the shipped bytes exactly. chrpath # can do this because it measures the slot by scanning the NULs it left behind # (chrpath.c: the rpathlen re-expansion loop), so this also pins that scan. check_out chrpath/restore-original 'new RPATH: $ORIGIN/lib-original-long-directory-name' \ chrpath -r '$ORIGIN/lib-original-long-directory-name' "$tmpd/a" check_eq chrpath/roundtrip-restores-bytes "$(sha "$tmpd/rpath64")" "$(sha "$tmpd/a")" # The one half of the ticket's criterion this image cannot execute. It is a skip # rather than a pass because nothing here ran an ELF, and rather than a red # because nothing is broken: there is no ELF loader on a wasm32 target, by # construction. The evidence is chrpath/rewrite-sha256 above. bst_skip chrpath/rewritten-still-runs \ "no ELF loader on this image (every executable is a wasm module); the rewritten binary was executed on x86-64 and the target reproduced its bytes exactly -- see chrpath/rewrite-sha256" # --------------------------------------------------------------------------- # RUNPATH is a different tag through the same code path, and chrpath reports # which one it found rather than calling both "RPATH". # --------------------------------------------------------------------------- cp "$tmpd/runpath64" "$tmpd/b" check_out chrpath/list-runpath 'RUNPATH=$ORIGIN/lib-original-long-directory-name' chrpath -l "$tmpd/b" check_rc chrpath/list-runpath-not-rpath 1 \ sh -c "chrpath -l '$tmpd/b' 2>&1 | grep -q ' RPATH='" check_out chrpath/replace-runpath 'new RUNPATH: $ORIGIN/lib' chrpath -r '$ORIGIN/lib' "$tmpd/b" check_eq chrpath/runpath-rewrite-sha256 \ 5fb7c09e6f7938e1b0b13fcddfd2b20f2d1acef27847628e8f9fef24a0fa27f1 "$(sha "$tmpd/b")" # --------------------------------------------------------------------------- # -c converts RPATH to RUNPATH by rewriting the d_tag in the dynamic section -- # the only edit chrpath makes outside .dynstr, and the only one that touches a # 64-bit field on this 32-bit target. # --------------------------------------------------------------------------- cp "$tmpd/rpath64" "$tmpd/c" check_out chrpath/convert "RPATH converted to RUNPATH" chrpath -c "$tmpd/c" check_out chrpath/convert-reads-back 'RUNPATH=$ORIGIN/lib-original-long-directory-name' chrpath -l "$tmpd/c" check_eq chrpath/convert-sha256 \ 51f8f1c7640d74f12708d3d52c2ec0bfa919b22d1b652c035fbd138dd7084496 "$(sha "$tmpd/c")" # Converting a file that is ALREADY RUNPATH is a documented no-op (the guard is # `d_tag == DT_RPATH`), so its bytes must not move. Asserted because a naive # implementation would rewrite the tag to the value it already holds and a # careless one would corrupt it. cp "$tmpd/runpath64" "$tmpd/c2" check_out chrpath/convert-noop-msg 'RUNPATH=$ORIGIN/lib-original-long-directory-name' chrpath -c "$tmpd/c2" check_eq chrpath/convert-noop-bytes "$(sha "$tmpd/runpath64")" "$(sha "$tmpd/c2")" # --------------------------------------------------------------------------- # -d removes the tag. It is silent on success, and the removal is observable # only by asking again -- so both halves are asserted. # --------------------------------------------------------------------------- cp "$tmpd/rpath64" "$tmpd/d" check_rc chrpath/delete-rc 0 chrpath -d "$tmpd/d" check_rc chrpath/delete-then-list-rc 2 sh -c "chrpath -l '$tmpd/d' >/dev/null 2>&1" check_msg chrpath/delete-then-list-msg "no rpath or runpath tag found" chrpath -l "$tmpd/d" check_eq chrpath/delete-sha256 \ 5293ab5ad07aeeedc8411712a431c6a9d61281627e456b278b27c1ef1d06c348 "$(sha "$tmpd/d")" # --------------------------------------------------------------------------- # ELF32: the class-dispatch axis. This is the fixture whose word size matches the # target's; the x86-64 ones above are the ones that do not, and both must work. # --------------------------------------------------------------------------- cp "$tmpd/rpath32" "$tmpd/e" check_out chrpath/list-rpath32 'RPATH=$ORIGIN/lib32-original-long-dir' chrpath -l "$tmpd/e" check_out chrpath/replace-rpath32 'new RPATH: $ORIGIN/lib' chrpath -r '$ORIGIN/lib' "$tmpd/e" check_out chrpath/reread-rpath32 'RPATH=$ORIGIN/lib' chrpath -l "$tmpd/e" check_eq chrpath/rpath32-size-unchanged 15040 "$(wc -c < "$tmpd/e" | tr -d ' ')" check_eq chrpath/rpath32-sha256 \ 687499df73d2af311e762c3b6c4b0d1e66d0bebe683615b311b4dd3f6d3d4093 "$(sha "$tmpd/e")" # --------------------------------------------------------------------------- # Refusals. Each one is asserted as status AND message: the statuses are 1, 2 and # 7 and they mean different things, so a tool that collapsed them to a single # nonzero would still pass a suite that only grepped the text. # --------------------------------------------------------------------------- # A replacement longer than the slot cannot be written in place, and the refusal # must leave the file untouched rather than half-written. cp "$tmpd/rpath64" "$tmpd/f" check_rc chrpath/toolong-rc 7 \ sh -c "chrpath -r '\$ORIGIN/lib-original-long-directory-name-and-then-some' '$tmpd/f' >/dev/null 2>&1" check_msg chrpath/toolong-msg "too large; maximum length 40" \ chrpath -r '$ORIGIN/lib-original-long-directory-name-and-then-some' "$tmpd/f" check_eq chrpath/toolong-leaves-file-alone "$(sha "$tmpd/rpath64")" "$(sha "$tmpd/f")" # A dynamic ELF with neither tag: status 2, distinct from "not an ELF" (1). check_rc chrpath/norpath-rc 2 sh -c "chrpath -l '$tmpd/nopath64' >/dev/null 2>&1" check_msg chrpath/norpath-msg "no rpath or runpath tag found" chrpath -l "$tmpd/nopath64" check_rc chrpath/norpath-replace-rc 2 \ sh -c "chrpath -r '\$ORIGIN/lib' '$tmpd/nopath64' >/dev/null 2>&1" # A file that is not an ELF at all. It must be longer than EI_NIDENT (16) or # chrpath takes its short-read path instead of its magic-number path, which is a # different diagnosis and a different message. printf 'this file is definitely not an ELF binary at all\n' > "$tmpd/notelf" check_rc chrpath/notelf-rc 1 sh -c "chrpath -l '$tmpd/notelf' >/dev/null 2>&1" check_msg chrpath/notelf-msg "probably isn't an ELF file" chrpath -l "$tmpd/notelf" # THE CASE THAT STATES THIS IMAGE'S SHAPE: chrpath pointed at its own installed # binary. /usr/bin/chrpath is a WebAssembly module, so the ELF editor correctly # declines to edit itself, and nothing in /usr/bin is a subject for this tool. check_msg chrpath/self-not-elf "probably isn't an ELF file" \ sh -c 'chrpath -l "$(command -v chrpath)"' check_rc chrpath/missing-rc 1 sh -c "chrpath -l '$tmpd/no-such-file' >/dev/null 2>&1" check_msg chrpath/missing-msg "No such file or directory" chrpath -l "$tmpd/no-such-file" # No arguments at all prints the usage and fails, rather than succeeding quietly. check_rc chrpath/noargs-rc 1 sh -c "chrpath >/dev/null 2>&1" check_msg chrpath/noargs-usage "Usage:" sh -c "chrpath 2>&1" # -k carries on past a file it could not read and still reports the failure in # its exit status; without it the second file is never reached. check_msg chrpath/keepgoing 'RPATH=$ORIGIN/lib-original-long-directory-name' \ sh -c "chrpath -k -l '$tmpd/notelf' '$tmpd/rpath64' 2>&1" check_rc chrpath/keepgoing-rc 1 \ sh -c "chrpath -k -l '$tmpd/notelf' '$tmpd/rpath64' >/dev/null 2>&1" check_rc chrpath/stops-without-k 1 \ sh -c "chrpath -l '$tmpd/notelf' '$tmpd/rpath64' 2>/dev/null | grep -q ORIGIN" rm -rf "$tmpd"