Links: hard links and symbolic links and what escapes the chroot
Linux knows two types of links and the difference between them is a sysadmin exam subject AND a chroot escape vector. The same concept that explains your data backup also explains how to break out of a jail.
The hard link
A hard link is a directory entry that points directly to an inode. The file and its link are two names for the same disk content. Deleting one does not delete the other (the inode survives as long as its link counter is above zero).
$ echo "test" > original.txt
$ ln original.txt hardlink.txt
$ ls -li original.txt hardlink.txt
12345 -rw-r--r-- 2 user user 5 Oct 26 10:00 original.txt
12345 -rw-r--r-- 2 user user 5 Oct 26 10:00 hardlink.txt
Same inode (12345), same content, link count at 2. Delete original.txt:
$ rm original.txt
$ cat hardlink.txt
test
The content survives. The counter went to 1. The file disappears when the counter reaches zero AND the last file descriptor is closed.
A hard link cannot cross a filesystem (an inode only exists in its filesystem) and cannot point to a directory (to avoid cycles, except . and .. which the kernel manages).
The symbolic link
A symbolic link (symlink) is a file that contains a path. It points to a path, not to an inode. If the target is deleted, the symlink becomes a dangling symlink: it exists but leads nowhere.
$ ln -s /path/to/original.txt symlink.txt
$ ls -la symlink.txt
lrwxrwxrwx 1 user user 25 Oct 26 10:00 symlink.txt -> /path/to/original.txt
The l at the start of the permissions indicates the symlink. The symlink can cross filesystems, point to a directory, point to a file that does not exist yet and cascade (a symlink to a symlink to a symlink).
The chroot and why symlinks think about it
A chroot changes the root directory of a process: / becomes for example /var/jail/. The process cannot access files outside the jail, because the kernel resolves paths from the new root.
Except symlinks are resolved AFTER the chroot and a symlink pointing to /etc/passwd inside the jail is resolved as /var/jail/etc/passwd. That works. But a symlink created BEFORE the chroot pointing to the real system’s /etc/passwd is resolved in the context of the real filesystem, not the chroot. The symlink crosses the jail because the path is stored as a string, not resolved at creation time.
# Inside the jail:
ln -s /etc/passwd /var/jail/escape_link
# After chroot into /var/jail:
# escape_link -> /etc/passwd
# But /etc/passwd is resolved from the REAL root if the symlink
# was created with an absolute path that goes beyond the jail
The countermeasure: create the symlinks AFTER the chroot (relative to the new root), or use pivot_root (which changes the kernel’s root, not just the process’s). This is exactly the lesson containers retained: the mount namespace isolates the VFS, the chroot isolates the path and symlinks cross the path but not the namespace.
The trio to retain
- Hard link: same inode, no cross-filesystem, no symlink inside a chroot, the content survives as long as link count > 0.
- Symlink: path stored, cross-filesystem, can point outside a chroot, can be dead (dangling), cascades.
- FIFO/socket/device: not links, but special files occupying an inode without disk content.
[edit: 2026] The chroot escape by symlink is still the CTF classic, but containers (Docker, LXC, Podman) solved the problem with mount namespaces (the process sees its own VFS, not just a different root) and rootless relies on user namespaces, which isolate more than chroot ever did. The symlink still crosses the path; it no longer crosses the namespace.