Chroot is largely a neat semantic hack that has come to be expected on Unix systems. Simply, when chroot is called with a pathname, that pathname becomes '/' for the process and it's descendants. Nothing more, nothing less.
It is worth noting that since it IS a semantic hack that was never originally envisioned as a security measure, several holes exist in it. It is also notable that many use chroot as a security measure, and it is even becoming part of 'best practices' for some daemons, particularly those that have been security problems in the past or that are frequently targets of attack.
In Linux, chroot is implemented entirely within path name lookup functions. That's reasonable since it is primarily a semantic hack. Simply, the __user_walk function accepts a path name string and performs a series of lookups within the VFS to return a dentry (a file system object more or less).
To implement chroot semantics, when it handles a leading '/', it substitutes in the dentry of the process's chroot. As it walks through the path, if it encounters '..' while already at the process's chroot, it skips that element (which is also a necessary behaviour when it is at the real root of the filesystem lest it walk off into space).
When a path contains no leading '/', it begins it's walk with the dentry for the current directory (cwd) which is also stored in a process's task structure.
The chroot system call does nothing but replace the root dentry in a process's task struct with the dentry for the specified path. A nice elegant system that avoids wasting time actually checking for corner cases including if the process is chrooted at all.
Unfortunately, this leads to a hole in chroot. The call to chroot does NOT change the CWD dentry for the process. This means that the process can hold on to a live dentry that is above the root dentry. The process may then chdir to a path consisting of a long series of '../' to end up at the real root of the filesystem.
do_path_lookup won't complain since it will never encounter the process's root dentry. Then, the process calls chroot('.') to reset it's root dentry to be the real root of the filesystem. Since chroot has no memory for previous root paths at all, a process doing that escapes form the 'jail' it was in and has the run of the system.
For an example, lets say a process running as root has been chrooted into /jail/virtual1 and it is at that root. This gives:
real CWD = '/jail/virtual1' apparent CWD = '/' chroot = '/jail/virtual1'
It now does:
mkdir('cake') real cwd = '/jail/virtual1' apparent cwd = '/' chroot = '/jail/virtual1'
chroot('cake') real cwd = '/jail/virtual1' apparent cwd = '/jail/virtual1' chroot = '/jail/virtual1/cake'
chdir('../../../../..') real cwd = '/' apparent cwd = '/' chroot = '/jail/virtual1/cake'
chroot('.') real cwd = '/' apparent cwd = '/' chroot = '/'
In response, as a hack on top of a hack, chroot is now generally limited to root (or CAP_SYS_CHROOT on capabilities systems) and daemons in a chroot jail are expected to run as non-root (a good idea anyway). The escape from chroot isn't fixed, but is restricted to root. Of course, the security of that measure depends entirely on a system with no local root exploits, including no exploitable suid root programs in the jail.
As we should all know, when it comes to security, those are not the best assumptions to be making.
The key to this 'get out of jail free card' is having a cwd outside of the jail. The escape above is widely publicized on security sites, but still oddly obscure elsewhere. However, the situation is actually worse than that.
In many UNIX systems (Linux included), one process may pass open file descriptors to another through a UNIX socket. An open file descriptor can be used to set the CWD through the fchdir system call. So, a non-root process can access files outside of it's chroot if it can get a little help from another non-root process that lives outside of the jail (even if the second process is itself in a jail above the escaping process).
The escape available for non-root processes is somewhat limited in that paths with a leading '/' will still refer to it's jail, but it can do plenty of damage using relative pathnames.
Consider, a jail is set up in /jails/virtual1. Further, a sub-jail is created in /jails/virtual1/subjails/virtual2.
Alice lives in /jails/virtual1. Bob has subverted a daemon chrooted to /jails/virtual1/subjails/virtual2.
Alice opens an fd to her '/' directory which is actually /jails/virtual1. Alice creates a unix socket named '/subjails/virtual2/cake'
Bob connects to /cake. Alice passes bob her open directory.
Bob calls fchdir on that open directory. He now sees his pwd as '/jails/virtual1'!!!
Bob calls chdir("../.."). His CWD is now the REAL ROOT OF THE FILESYSTEM.
As an added touch, Bob can now boost Alice out of jail as well.
If either Bob or Alice have the necessary libraries within their chroot jail, they can potentially exploit a suid root binary in the real root filesystem to complete their escape. Otherwise, they STILL have a lot of access to the system that they were not intended to have. If the sysadmin was relying on chroot to keep them out, security in the real '/' may not be all it should be.
It really doesn't matter that chroot isn't 'officially' defined as a security measure. It IS USED as one all the time and is even coming to be accepted as a best practice. For example, by default, Fedora places named in a chroot jail now.
The Groucho Marx approach of 'so don't do that' will not do. Chroot is becoming popular exactly because it is a useful functionality provided that these holes are closed.
The harden_chroot patch does exactly that. Simply, the patch causes chroot to ALSO chdir to the specified directory. In addition, it adds a function that walks upwards from any dentry until it either encounters the process's root or the real filesystem root. That check is called by fchdir and the path lookup functions. If the process root is not encountered, the system call fails, so that having an open descriptor or CWD outside of your root is no longer useful for escaping from jail.
This patch MAY cause unexpected results IF some program depended on being passed an open directory outside of it's jail, but given that such a procedure renders chroot practically useless, it probably deserves to fail! The correct way to give a chroot program access to an additional directory is to bind mount it inside the jail.
Systems depending on passing open regular files or devices to a jailed program will continue to function normally.
— Steven James 2007/01/13 12:54