fix(debian/launcher): patch the ELF interpreter
QEMU relative paths
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
inqemu_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:
$ 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:
$ 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
The solution
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
Here be dragons
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.
Python Debian Launcher
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@