Skip to content

fix(debian/launcher): patch the ELF interpreter

Matthew Clarkson requested to merge patchelf into main

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 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:

$ 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 @
Edited by Matthew Clarkson

Merge request reports