fix(debian/launcher): patch the ELF interpreter
We have an issue with QEMU relative paths. QEMU uses relative paths to the binary directory. The process is as so: - QEMU uses `/proc/self/exe` in `qemu_init_exec_dir` to get the current executable path - That usually returns the location of `qemu-system-{arm,aarch64,i386,x86_64}` - Relocatable paths, such as `QEMU_MODULE_DIR` are then found relative to the directory However, because we can invoking the binary with the ELF interpreter `/proc/self/exe` resolves the ELF interpreter path rather than the QEMU binary path. This throws off all of the relative paths that QEMU uses to find the data files. Concretely, this manifests as: ```console $ bazelisk run -- debian/amd64/qemu-system-arm:qemu-system-aarch64 -L help /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/unpack/lib/x86_64-linux-gnu/../share/qemu /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/unpack/lib/x86_64-linux-gnu/../share/seabios /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/unpack/lib/x86_64-linux-gnu/../lib/ipxe/qemu ``` Rather than: ```console $ bazelisk run -- debian/amd64/qemu-system-arm:qemu-system-aarch64 -L help /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/patched/usr/bin/../share/qemu /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/patched/usr/bin/../share/seabios /home/matt-clarkson/.cache/bazel/_bazel_matt-clarkson/8a909ce9ddae247581c15927c03304e3/execroot/_main/bazel-out/k8-fastbuild/bin/debian/amd64/qemu-system-arm/patched/usr/bin/../lib/ipxe/qemu ``` To resolve the paths correctly, we need to override the ELF interpreter path in the `PT_INTERP` ELF header to point at the downloaded Debian ELF interpreter. `patchelf` tool allows us to do that with `--set-interpreter`. The issue is that we cannot concretely set the path because the Bazel sandbox will always mount the Debian packages at a different absolute path. Linux does not support `${ORIGIN}` in `PT_INTERP`. Solaris does. This patch employs the following strategy: - Unpack Debian archives - Whilst unpacking the archives, yield any executable files - Use `patchelf` to set a fixed ELF interpreter path under `/tmp` - In the Debian launcher symlink the ELF interpreter under `/tmp` to the real Debian ELF interpreter This works but there is a race condition: Bazel does not necessarily mount a fresh `/tmp` into each sandbox so two actions running at the same time will thrash on the symlink. The patch atomically moves the symlink into place and each action running requiring the same symlink will always point at a concrete ELF interpreter. It is possible that the following could happen: - Action 1 starts and sets the ELF interpreter symlink to `execroot/1/ld-linux.so.2` - Action 2 starts and sets the ELF interpreter symlink to `execroot/2/ld-linux.so.2` - Commands from action 1 will still work because the second symlink points to the compatible ELF interpreter - Action 2 ends, unmounting the real ELF interpreter - Action 1 attempts to start a binary from within the already running binary - The ELF interpreter fails to resolve This is unlikely to happen because the launcher symlinks then launches the binary. In our usage, QEMU _likely_ won't start other host executables once it has launched. I will follow-up with a better strategy for the interpreter symlink but feel the risk is low enough to land this now. This patch also changes the Debian launcher to use Python rather than Bash to launch the executable. This allows us to use the robust Python runfiles library and not have to use the janky Bash runfiles library that requires unhermetic tools to find it. To do this it employs a trick that we can use in other modules to avoid using Bash in the future: - Write out baked executable arguments to a file - Use `ctx.runfiles#root_symlinks` to link the file to the root of the runfiles - Load the baked arguments using the runfiles library using the consistent symlink name - Add the file onto the front of the passed in arguments with an `@` symbol - Make sure `ArgumentParser#fromfile_prefix_chars` is set to `@`