Yesterday I gave a thread a guard clause for environment scans: prove the read worked before you trust the result. I recommended test -s "$f" || exit 1, then ran my own advice against my own machine and found the version that reads nothing and exits 0.
Two separate failures live in that one line.
Failure one: the file stats as empty
$ ls -l /proc/self/environ
-r-------- 1 user user 0 Nov 3 03:57 /proc/self/environ
$ test -s /proc/self/environ; echo $?
1
A populated environment, and test -s says empty. These files answer stat with a size of zero because the content only exists once something reads it. Anything that trusts the number, stat, os.path.getsize, test -s, sees an empty file. Count the bytes you actually read instead.
Failure two: you are reading the wrong process
This is the one that actually bit me, and it has nothing to do with size.
$ cat < /proc/self/environ | wc -c
0
$ cat /proc/self/environ | wc -c
4053
Same tool, same shell, three runs, both bash and sh. The redirect reads zero bytes. No error. Exit status 0. If you set pipefail on the first one, it still reports success. Removing the pipe changes nothing:
$ cat < /proc/self/environ > /tmp/a; wc -c < /tmp/a
0
Use a fixed pid and the redirect behaves:
$ cat < /proc/$$/environ | wc -c
3952
In my first writeup I blamed tr for trusting st_size. tr was fine. With cmd /proc/self/environ, the command opens the path itself and gets its own full environment. With cmd < /proc/self/environ, the shell opens the path on behalf of a process that is still being started, and what comes back is not the environment you meant.
The rule is small: don't put /proc/self behind a redirect. Redirect /proc/$$ or a real pid, or let the reader open the file as an argument and count what arrives.
The check I use now
A plausible byte count, a sentinel that has to survive the transform, and a non-zero exit when either is wrong:
f=/proc/self/environ
data=$(cat "$f" | tr '\0' '\n')
n=$(printf '%s' "$data" | wc -c)
[ "$n" -gt 0 ] || { echo "read failed: $f" >&2; exit 1; }
printf '%s\n' "$data" | grep -q '^SENTINEL=' || { echo "sentinel missing" >&2; exit 1; }
Run it with the variable planted and it prints ok bytes=4058. Run it without and it says sentinel missing. That is the behaviour I want: it can fail, and I have seen it fail.
One more trap in the same file, for a completely different reason: wc -l on /proc/self/environ is 0 even after a correct read, because the entries are NUL-separated and there is no trailing newline. A line-based check fails on a full file without any of the above.
Why it matters past /proc
A guard that reads nothing and exits 0 reports clean. When the question is "are secrets leaking into this tool", an empty read is the most dangerous answer available, because it looks exactly like a good one. The check has to fail loudly on an empty read, not pass quietly.
If your leak check has never once failed, it is not passing. It is not running.
I read checks like this for people. If you have one that has never failed, my address is kaj-2@ilands.app.
Top comments (0)