61

I have a unix installation that's supposed to be usable both as a chroot and as a standalone system. If it's running as a chroot, I don't want to run any service (cron, inetd, and so on), because they would conflict with the host system or be redundant.

How do I write a shell script that behaves differently depending on whether it's running in a chroot? My immediate need is a modern Linux system, with /proc mounted in the chroot, and the script is running as root, but more portable answers are welcome as well. (See How do I tell I'm running in a chroot if /proc is not mounted? for the case of Linux without /proc.)

More generally, suggestions that work for other containment methods would be interesting. The practical question is, is this system supposed to be running any services? (The answer being no in a chroot, and yes in a full-fledged virtual machines; I don't know about intermediate cases such as jails or containers.)

5 Answers5

65

What I've done here is to test whether the root of the init process (PID 1) is the same as the root of the current process. Although /proc/1/root is always a link to / (unless init itself is chrooted, but that's not a case I care about), following it leads to the “master” root directory. This technique is used in a few maintenance scripts in Debian, for example to skip starting udev after installation in a chroot.

if [ "$(stat -c %d:%i /)" != "$(stat -c %d:%i /proc/1/root/.)" ]; then
  echo "We are chrooted!"
else
  echo "Business as usual"
fi

(By the way, this is yet another example of why chroot is useless for security if the chrooted process has root access. Non-root processes can't read /proc/1/root, but they can follow /proc/1234/root if there is a running process with PID 1234 running as the same user.)

If you do not have root permissions, you can look at /proc/1/mountinfo and /proc/$$/mountinfo (briefly documented in filesystems/proc.txt in the Linux kernel documentation). This file is world-readable and contains a lot of information about each mount point in the process's view of the filesystem. The paths in that file are restricted by the chroot affecting the reader process, if any. If the process reading /proc/1/mountinfo is chrooted into a filesystem that's different from the global root (assuming pid 1's root is the global root), then no entry for / appears in /proc/1/mountinfo. If the process reading /proc/1/mountinfo is chrooted to a directory on the global root filesystem, then an entry for / appears in /proc/1/mountinfo, but with a different mount id. Incidentally, the root field ($4) indicates where the chroot is in its master filesystem.

[ "$(awk '$5=="/" {print $1}' </proc/1/mountinfo)" != "$(awk '$5=="/" {print $1}' </proc/$$/mountinfo)" ]

This is a pure Linux solution. It may be generalizable to other Unix variants with a sufficiently similar /proc (Solaris has a similar /proc/1/root, I think, but not mountinfo).

Pablo A
  • 2,712
  • 1
    This won't work in OpenBSD because it has random PIDs; the root process is basically never PID 1. Now you know why! – Adam Katz Jan 16 '15 at 05:59
  • @AdamKatz "... with a couple of obvious exceptions, e.g., init(8)." So which is it? – muru Jan 16 '15 at 09:40
  • @muru: aw, shucks. You've shot me down. I'm not sure why init(8) would absolutely need to have the #1 slot unless there's some kind of hard-coded nature that requires it (in which I'd still be unsure as to why). Of course, the BSDs have much more advanced jails than just chroot, so I'm not even sure how problematic this is. – Adam Katz Jan 16 '15 at 10:02
  • 5
    @AdamKatz It's the opposite: pid 1 has a special role (it must reap zombies, and it is immune to SIGKILL). The init program is an implementation of that role. The reason my answer doesn't work in OpenBSD has nothing to do with this: it's because OpenBSD doesn't have anything like Solaris/Linux's /proc. My answer wasn't meant to address anything but Linux anyway. – Gilles 'SO- stop being evil' Jan 16 '15 at 11:40
  • @Gilles I figured OpenBSD would have this defeated in some manner or other. Still, I'm surprised that all of those special role items aren't capable of being applied to an arbitrary PID (without consequences), which is what I meant in my italicized "why" earlier. – Adam Katz Jan 16 '15 at 17:11
  • Well done, I had to add sudo for perm on my system, here is your idea in an alias: alias ischroot='[ "$(sudo stat -c %d:%i /)" != "$(sudo stat -c %d:%i /proc/1/root/)" ] && echo "Yes, we are chrooted!" || echo "No chroot detected."' – Adam D. Jan 14 '16 at 02:49
  • @Gilles There is mount_procfs, you can then use ps and grep to find the init pid. – Adam D. Jan 14 '16 at 03:02
  • You mention checking mountinfo for both PID ## and 1. Why? – sbhatla Jun 10 '17 at 04:12
  • @sbhatla To compare them. The information a single mountinfo file isn't a good test for the process being chrooted. But if the information differs for process 1 and for the running process, that shows that the running process is running in a different root from process 1. – Gilles 'SO- stop being evil' Jun 10 '17 at 19:17
  • Having /proc mounted is clearly a security flow, as you can access the real root throw "ls -l /proc/1/root/." – Vouze Jan 11 '18 at 09:55
  • 2
    @Vouze No, having /proc mounted is not a security flaw, because chroot alone is not a security mechanism. You can get security from chroot only if the processes running in the chroot run with separate user IDs from processes running outside the chroot. Otherwise chroot does not protect you, for example, from a process that kills or ptraces another process that's running outside the chroot. In that case /proc/$pid/root does not permit bypassing the chroot. – Gilles 'SO- stop being evil' Jan 11 '18 at 19:00
  • Nice answer; I managed to avoid the subshell substitutions and [/test with a simple awk command: if stat -c %d:%i / /proc/1/root/. | awk -v RS= '{ exit $1 != $2; }'; then (setting record separator to empty string automatically makes newline a field separator). Even easier if your test has the -ef (same file) comparison! – Toby Speight Mar 14 '19 at 13:52
30

As mentioned in Portable way to find inode number and Detecting a chroot jail from within, you can check whether the inode number of / is 2:

$ ls -di /
2 /

An inode number that's different from 2 indicates that the apparent root is not the actual root of a filesystem. This will not detect chroots that happen to be rooted on a mount point, or on operating systems with random root inode numbers.

l0b0
  • 51,350
  • 1
    On what filesystems does this heuristic work? – Gilles 'SO- stop being evil' Nov 09 '11 at 11:36
  • Tested on ext3 and hfs. – l0b0 Nov 09 '11 at 11:52
  • So I was fooling around, and I think I've found a more reliable method that doesn't require root permissions (Linux only). I'm still open to counter-examples or more portable methods. – Gilles 'SO- stop being evil' Nov 09 '11 at 19:15
  • 6
    This is true for ext[234], but not of all filesystems. It also only tests that your root is the root of the filesystem, which may not be mounted as the real root. In other words, if you mount another partition in /jail and chroot /jail, then it will look like the real root to this test. – psusi Nov 09 '11 at 19:26
  • @Gilles I just wanted to bring your attention to psusi's point (since you were not notified) - the inode number of each separate, "real" filesystem will be 2. (Exceptions include, for instance, tmpfs. I haven't investigated much further.) – rozcietrzewiacz May 29 '12 at 13:14
  • @rozcietrzewiacz Thanks. I had psusi's concern too: the use case that led me to ask this question was a Linux installation that is designed to work both standalone and as a chroot, so it's on its own partition. The inode heuristic wouldn't work there, but it's a useful one in some controlled environements. – Gilles 'SO- stop being evil' May 29 '12 at 17:33
  • I noted that OpenBSD has random PIDs in a comment to Gilles's answer. Similarly, OpenBSD has random inode numbers (same link), which defeats this technique. The more I learn about OpenBSD, the more I like it... – Adam Katz Jan 16 '15 at 06:03
  • This works well for me, detects perfectly for Crouton running Ubuntu over Chromeos without any need for superuser privileges. alias ischroot='(($(ls -di / | cut -d" " -f1)==2)) && echo "No chroot detected." || echo "Yes, we are chrooted!"' – Adam D. Jan 14 '16 at 02:58
  • 1
    @AdamKatz Apparently not. Tested in openbsd 6.0-stable, the inode number is still 2 for the actual root path while it's a random number for the chroot. – Dmitri DB Sep 16 '16 at 03:11
13

While clearly not as portable as many other options listed here, if you're on a Debian-based system, try ischroot.

See: https://manpages.debian.org/jessie/debianutils/ischroot.1.en.html

To get the status in the console directly, using ischroot:

ischroot;echo $?

Exit codes:

0 if currently running in a chroot
1 if currently not running in a chroot
2 if the detection is not possible (On GNU/Linux this happens if the script is not run as root).
terdon
  • 242,166
thom_nic
  • 547
  • 2
    Since ischroot is OSS (under GPL2), you can download the source from the official (currently here) and ./configure && make ischroot to compile it. Tested under Arch Linux. – ynn Apr 26 '20 at 22:58
3

Use systemd-detect-virt -r. (must be root)

1

This is just an accidental observation. But if you have access to anything using FUSE, then you can mount something and then use mount to show the mount path, which will be shown with the chroot path prefixed.

E.g., on the Github Actions Mac runner you can do something like:

pip install --user ratarmount
folder=$(mktemp -d)
mountedFolder=$(mktemp -d)
# simple bind mount a folder
ratarmount "$folder" "$mountedFolder"

Now mount will print something like:

TarMount on /private/var/folders/24/8k48jl6d249_n_qfxwsl6xvm0000gn/T/tmp.P5g4TomF (macfuse, nodev, nosuid, synchronous, mounted by runner)

even though the full path in the mountedFolder variable is: /var/folders/24/8k48jl6d249_n_qfxwsl6xvm0000gn/T/tmp.P5g4TomF. Therefore it seems we are chrooted into /private.

Because of this the hacky mountpoint command replacement using mount and grep did not work for me.

mxmlnkn
  • 556
  • 5
  • 11