The beauty of Unix

2026-01-12T19:50:41

One of the greatest advantages of using a Unix system is its openness and intelligibility: it rewards users who invest time into knowing and exploring its design. Just going about daily life using your computer offers the opportunity to learn about technical concepts that are elegant and (you could say) beautiful, not to mention extremely useful and applicable in many situations, personal as well as professional — though sadly this type of knowledge is not as common as it should be. Well, that is why this blog exists.

/proc/$pid/exe

This first case came about when I upgraded some operating system packages. Apparently, some version mismatch between the tmux server and client caused all new operations to fail:

$ tmux attach
open terminal failed: not a terminal

Since I was not in the mood to restart all my sessions at that time, my first thought was to downgrade the package. Unfortunately, it was no longer in the pacman cache. Then came an epiphany: among the many useful files in the proc(5) file system is exe, which is (or manifests itself to use space as) a symbolic link to the executable file which was used to start a particular process:

$ ls -l /proc/self/exe
lrwxrwxrwx 1 bbguimaraes users 0 Jan 12 21:04 /proc/self/exe -> /usr/bin/ls

Files in Unix operating systems have a reference count, meaning they are only deleted when they are no longer reachable. Uninstalling the older version of the tmux package eliminated all references in the file system, but the file still exists since it is referenced by the process table, and can be named using the proc file (this is why it is not exactly a symbolic link, which only refer to other paths):

$ ls -l /proc/$(pgrep --oldest tmux)/exe
lrwxrwxrwx 1 bbguimaraes users 0 Jan 10 16:28 /proc/1390/exe -> '/usr/bin/tmux (deleted)'

Here (deleted) means there are no longer file system references to the file. But since we can name the file, we can use that just as any other file path, including to start new processes:

$ /proc/$(pgrep --oldest tmux)/exe new-session …

This enabled me to keep using my existing tmux sessions without having to restart the server just because of a package update. This technique is not perfect, of course. It worked because the only incompatibility was between the two main executable files: things would be different if any other incompatibility existed between the two versions (and maybe there is, and I have not run into it yet).

ptrace

This second case came from the need to add a new configuration variable to my .bash_profile. The specific situation is not terribly important, but I needed this new variable to be propagated to my window manager (i3) so that it would be set when a specific graphical program was started. It has already been established that I do not like to stop everything I am doing just to restart some piece of software, and the idea of recreating my entire graphical session was very unappealing.

Unix systems have an interface through which a process can examine another: ptrace(2). The obvious case where this is useful is a debugger, and this is the system call GDB and friends use to manipulate other processes. While in most cases a debugger is used to start that process as a child, inspect its running state, and pause/resume execution, the interface allows much more.

First, the attached process does not have to necessarily be a child (though a specific capability, CAP_SYS_PTRACE is required for this, for security reasons). For example, starting gdb with the --pid argument will attach to an existing process:

# gdb --pid 1000
Attaching to process 1000
Reading symbols from /usr/bin/sleep...

(No debugging symbols found in /usr/bin/sleep)
0x00007f641da9318e in ?? () from /usr/lib/libc.so.6
(gdb)

The entire execution context is at the mercy of the controlling process, which can choose to alter it in any way. This is how the break, continue, etc. commands are implemented in debuggers: they alter the code itself being executed. If you have ever used the print command in GDB to print the result not of an expression, but of some non-trivial piece of code, this is also what is happening. Since it actually executes code in the child's context, those types of commands can be used to alter the state of the process (now you can see why ptrace is protected by a capability).

Armed with this knowledge, we can go back to i3 and use gdb to politely coerce it to change its environment while it is being executed:

# gdb --pid $(pgrep --oldest i3)
…
(gdb) print (void)putenv("GTK_THEME=Adwaita:dark")
(gdb) detach

environment

Those who know proc might be tempted to look at the environ file to verify that the environment was indeed changed by putenv(3), but that will not work: that file reflects the state as it looked when the program began execution, and is not affected by the addition of new variables. This can be verified with a simple C program:

#define _POSIX_C_SOURCE 200112L
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **envp) {
    setenv("test", "test", 0);
    printf("%p %p", *envp, envp);
    while(*++envp);
    printf(" %p\n%p\n", envp, getenv("test"));
}

// sample output:
// 0x7ffc423b10e1 0x7ffc423aff38 0x7ffc423b0108
// 0x55f053ee95a5

As can be seen from the sample output (note that, because of ASLR, the specific addresses will change from execution to execution), the initial environment and the new variable added by setenv(3) are placed in completely different memory regions. We can verify that those are, respectively, the program stack and the heap by inspecting yet another proc file:

$ grep '7ffc\|55f05' /proc/$(pgrep a.out)/maps | column --table
55f053ee9000-55f053f0a000  rw-p  00000000  00:00  0  [heap]
7ffc42391000-7ffc423b2000  rw-p  00000000  00:00  0  [stack]
filesystem gdb linux pacman proc ptrace strace tmux unix