Using chroot jails is another typical containment issue.
The chroot system call changes the virtual file system root to
another directory for the process itself, and all subprocesses created
by it. Once you've called chroot, all file names starting with
a slash are interpreted relatively to this directory. Also, using
.. at the new root directory will not take you outside the
chroot area. So, in terms of file system operations such as opening
files or executing programs, the process is confined to the new
root directory. For this reason, this is often referred to as a
jail.11.8 However, a chroot environment does not provide perfect
insulation; processes within the jail are still able to send signals to,
and even ptrace other processes that run under the same uid.
Note that for security reasons, only the super user is allowed to use the chroot call.
The standard example for this technique is the FTP daemon, which changes it's file system root to /home/ftp or similar. The idea is that even if there's a bug in the FTP daemon, the attacker will not be able to access files outside the chroot area.11.9 Other services that also benefit from putting them into a jail are portmapper, dhcpd, tftpd, etc.
There are several gotchas you have to be aware of:
The first issue is that after doing the chroot call, make sure to call chdir(/). Assume your current directory is /tmp, and you change the file system root to /home/jail. Since file names that do not start with a slash are still interpreted relative to your current working directory, using ../etc/passwd will still allow you to see the system's password file.
The second issue is that, for the root user, it is trivial to break free from a chroot jail. Consider the following piece of code:
chdir("/");
mkdir("hole");
chroot("hole");
chdir("../..");
chroot(".");
Assume this code is executed from a process running inside the chroot jail /home/ftp.
The chroot call (which succeeds because the caller is root) sets the file system root to /home/ftp/hole, and the previous root directory is forgotten. The current working directory of the process is /home/ftp however, and hence outside the chroot area. Therefore, the relative path using in the chdir call will take us up to the real filesystem root. The final chroot call sets the process's root directory to the real file system root.
So what you have to do is drop privilege, but only after doing the chroot call because that requires root privilege. However, inside a chroot directory, library functions such as getpwnam and initgroups usually don't work.11.10 So there's basically one canonical was of doing it which looks like this:
struct passwd *pwd;
/* Get user info */
pwd = getpwnam(jailuser);
if (pwd == NULL || pwd->pw_uid == 0)
faial("bad jail user %s", jailuser);
/* Now set up the chroot jail */
if (chroot(jaildir) < 0 || chdir("/") < 0)
fatal("jail setup failed: %m");
/* Finally, drop privilege */
if (setgroups(1, &pwd->pw_gid) < 0
|| setgid(pw->pw_gid) < 0
|| setuid(pw->pw_uid) < 0)
fatal("failed to drop privs: %m");
Note that there's no 100% guarantee that non-root users are able to break out of a chroot jail. For instance, earlier implementations of the Linux /proc filesystem, when mounted below a chroot area, allowed any user to break free simply by doing a cd /proc/1/cwd. Also, if a process is able to obtain an open file descriptor to a directory outside the chroot area, it can escape by calling an fchdir with that descriptor.