procfsroot
is a small Go module that helps with accessing file system paths
containing absolute symbolic links that are to be taken relative (sic!) to a
particular root path. A good example is accessing paths inside
/proc/[PID]/root
"wormholes" in the proc file
system. Symbolic links are
properly resolved and kept inside a given root path, prohibiting rogue relative
symbolic links from breaking out of a procfs root wormhole.
For devcontainer instructions, please see the section "DevContainer" below.
procfsroot.EvalSymlinks()
mirrors Golang's
filepath.EvalSymlinks
,
but works only on paths using "/
" forward slashes and enforces symbolic link
chasing relative to an enforced root path.
In the following example, the "absolute" path /var/run/docker.sock
(which
might be in a different mount namespace) is correctly resolved in the root
context of /proc/1/root
– even in case of absolute symbolic links, such as
/var/run
usually being an absolute symlink pointing to /run
. Trying to
directly use /proc/1/root/var/run/docker.sock
will fail in case of different
mount namespaces between the accessing process and the initial mount namespace
of the init process PID 1, as this would be resolved by the Linux kernel into
/run/docker.sock
in the current mount namespace(*).
import (
"os"
"github.com/thediveo/procfsroot"
)
const root := "/proc/1/root"
func main() {
p, err := procfsroot.EvalSymlinks("/var/run/docker.sock", root, procfsroot.EvalFullPath)
if err != nil {
panic(err)
}
f, err := os.Open(root + p)
defer f.Close()
}
For illustrational purposes, simply run this as an "incontinentainer" to show that absolute symbolic path access will fail when done through a wormhole:
$ docker run -it --rm --pid=host --privileged busybox ls -l /proc/1/root/var/run/docker.socket
ls: /proc/1/root/var/run/docker.socket: No such file or directory
In case you have either never noticed the special /proc/[PID]/root
links or
have ever wondered what they're good for: they're kind of "wormholes" into
arbitrary mount
namespaces given
a suitable process ID (PID). They simplify accessing directories and files in
other mount namespaces because they do not require switching the accessing
process first into the target mount namespace (which can only be done while
single threaded).
Access Method | Required Capabilites |
---|---|
setns() |
CAP_SYS_ADMIN , CAP_SYS_CHROOT , as well as typically also CAP_SYS_PTRACE in order to access a mount namespace reference in /proc/[PID]/ns/mnt |
/proc/[PID]/root |
CAP_SYS_PTRACE (so convenient 😀) |
Of course, the usual file system DAC (discretionary access control) still applies as usual – including UID 0 access rules.
Also, for access to /proc/[PID]
the current process needs to be in a suitable
PID namespace
that includes the PID of a "target" process of interest. Of course, the initial
PID namespace is "gold standard".
Caution
Do not use VSCode's "Dev Containers: Clone Repository in Container
Volume" command, as it is utterly broken by design, ignoring
.devcontainer/devcontainer.json
.
git clone https://github.com/thediveo/procfsroot
- in VSCode: Ctrl+Shift+P, "Dev Containers: Open Workspace in Container..."
- select
enumflag.code-workspace
and off you go...
The included procfsroot.code-workspace
defines the following tasks:
-
Build workspace task: builds all, including the shared library test plugin.
-
Run all tests with coverage task: does what it says on the tin and runs all tests with coverage.
make
: lists available targets.make test
: runs all tests.make coverage
: deprecated, use thegocover
CLI command in the devcontainer instead.make report
: deprecated, use thegoreportcard-cli
CLI command in the devcontainer instead.
Copyright 2021, 2025 Harald Albrecht, licensed under the Apache License, Version 2.0.