// notes
Who calls unshare: intercepting a binary through PATH
A command fails without naming the culprit. Here is the trick: replace the binary with a logging script via PATH and find the caller without tracing or root
A command fails, but the message does not name the process that called it. In that situation a simple trick helps: replace the binary with a logging script and put it first in PATH (the list of directories where the shell looks for commands). I use unshare as the example, but the method works for any command
Problem: a message with no culprit
A classic scenario: direnv exec . in a project with use flake fails with unshare: unshare failed: Operation not permitted. The error is printed by unshare, but who called it is not stated. It could be nix-direnv (direnv's plugin for Nix environments), a wrapper around nix, a direnv hook, or something else
Searching configs by hand can take a while. You need a way to catch the call at the moment it happens
The error names the tool, not the caller
Why strace is not always a good fit
strace (a system call tracer) with -e trace=execve will show every process launch, including unshare. The downside is the volume: the call you want drowns in output, and ptrace settings (the mechanism a tracer uses to attach to a process) do not always allow it. For a quick search, intercepting one command is enough
Cause: interception through PATH
The shell searches the PATH directories left to right and runs the first executable file with a matching name. So a directory with your own script at the front of PATH gets the call before the real binary. The trick was needed because the error is printed by the tool (unshare), not by whoever called it. In the case I traced the culprit was a wrapper around nix that raised the namespace a second time: nix-direnv: "unshare failed: Operation not permitted"
The trick: replace the binary
PATH resolves a command by name: the shell takes the first executable file with a matching name. So you only need to put your own unshare script in a separate directory and put that directory at the front of PATH
All commands below run in the same terminal
HOOKDIR="$(mktemp -d)"
cat > "$HOOKDIR/unshare" <<EOF
#!/bin/sh
echo "UNSHARE CALLED: \$@" >> "$HOOKDIR/unshare.log"
exit 1
EOF
chmod +x "$HOOKDIR/unshare"
export PATH="$HOOKDIR:$PATH"
direnv exec . true
cat "$HOOKDIR/unshare.log"We do not quote EOF: that way $HOOKDIR is expanded when the script is created, and the log gets an absolute path. \$@ is escaped so it stays in the script and is expanded when the script runs
The script writes the arguments to the log and exits with exit 1. This imitates a failure rather than reproducing the original EPERM: the call does not continue down the chain, and the log records who made it and with which arguments
After debugging, restore the environment:
rm -rf "$HOOKDIR"Verify: reading the log
One caveat up front: the log shows the call arguments, not the process name. The caller is what we look for next, in your own directories
grep -rn "unshare" "$HOME"/.local/bin/ "$HOME"/.config/direnv/ 2>/dev/nullThe log will contain a line like UNSHARE CALLED: --mount --map-root-user .... The arguments show how the namespace is raised (an isolated environment at the kernel level). The call itself narrows the search: next, find who could have called unshare
In my case the log showed my own wrapper around nix, which called unshare a second time. Another similar case, the silent PATH substitution from the devShell in fish, is covered in Nix devShell silently loses packages in fish
You can confirm the culprit by re-running: remove the interception, fix the place you found, and repeat the failing scenario. If the unshare failed error is gone, that was the caller
Pitfalls
- It only catches calls by name through
PATH. A command invoked by an absolute path (/usr/bin/unshare) bypasses the substitution - It does not work if the name is taken by a shell function or alias: the shell runs those instead of the binary from
PATH - It does not see calls with a cleared environment (
env -i) or launches throughsudo: by defaultsudoresetsPATH(env_reset) - It does not show already running processes and is no replacement for
straceor system auditing
Takeaway
The trick needs no root: replace the command with a logging script, put its directory first in PATH, reproduce the failure, and read the log. Once the culprit is found, you fix one specific place instead of the whole environment
Related notes
Nix devShell in fish: PATH is silently ignored
devShell packages are not picked up in fish because PATH points inside the mount namespace. How to diagnose the silent fallback and re-exec the shell
nix-direnv: "unshare failed: Operation not permitted"
direnv with use flake fails on unshare failed while dotenv works. I trace the cause (a nested user namespace) and show the fix for the nix wrapper
nix-direnv: "minimum required bash version is 4.4"
nix-direnv fails with "minimum required bash version is 4.4" even though bash is newer. I break down the hard-coded /nix/store paths, the cause, and the fix