Wandering Thoughts archives

2015-08-28

The somewhat surprising history of chroot()

The chroot() system call is one of those that I think of as being really old. Up until a while back, if you'd pressed me I'd have guessed that it originated in V7. Then I dug into it and in that tweet claimed it was from 4.2 BSD. This is what you'd kind of expect and what it sort of looks like, but it turns out to be wrong and the history of chroot() seems to be much more interesting than I expected.

The classic use of chroot() is for safely confining network daemons, and indeed this is what ftpd uses it for in 4.2 BSD. But there's another use of it in the 4.2 BSD tree lurking in a file called /usr/src/games/compat/runcompat.c. As a manpage and a Readme there make clear, this is a system to let you run PDP-11 V6 and V7 programs on your VAX. It uses chroot() to optionally put those programs inside a familiar V6 or V7 directory hierarchy. This appears to go more or less all the way back to 32V, the first Unix version to run on a VAX and thus the first one where this desire would come up. But it's not where chroot(2) was added.

Much to my surprise, it turns out that chroot(2) is in V7 after all. It's not obvious because there's no separate manpage for it; instead it's part of the chdir(2) manpage. I don't know why it's there, apart from 'someone thought it was an obvious feature'; tuhs.org has more or less complete V7 source, and there's no programs in the source tree that use it that I can find. Apparently it was added first and then people found various uses for it later. What we now think of as its most prominent uses (for things like confining anonymous ftp) are relatively latecomers to the game, since they started years after chroot() itself came into existence.

(I don't believe that the V7 chroot() was a security measure.)

Sidebar: why chroot is in the chdir manpage

V7 implements chdir(2) and chroot(2) with basically the same code; you can see it in /usr/sys/sys/sys4.c. It's pretty clever:

chdir()
{
    chdirec(&u.u_cdir);
}

chroot()
{
    if (suser())
        chdirec(&u.u_rdir);
}

chdirec(ipp)
register struct inode **ipp;
{
[...]

In other words, the only real difference between chdir and chroot is what field in the u user structure they act on. chdir acts on the 'current directory' field, chroot acts on the 'root directory' field.

The actual implementation of chroot requires a bit more kernel code than this, because of course the u_rdir field didn't exist before chroot was added. But there turn out to be remarkably few places that deal with u_rdir in the V7 kernel. By the way, reading this code makes me suspect that a V7 chroot was remarkably permeable; I don't see any reason why you can't just 'cd ..' your way right out of one (although I may be missing something subtle in the code). That obviously changed later.

(Note that the V7 manpage doesn't claim that chroot() confines processes; it simply says that it changes what '/' means to them. Which is literally what the kernel code does.)

ChrootHistory written at 02:42:31; Add Comment

2015-08-17

Getting dd's skip and seek straight once and for all

Earlier today I wanted to lightly damage a disk in a test ZFS pool in order to make sure that some of our status monitoring code was working right when ZFS was recovering from checksum failures. The reason I wanted to do light damage is that under normal circumstances, if you do too much damage to a disk, ZFS declares the disk bad and ejects it from your pool entirely; I didn't want this to happen.

So I did something like this:

for i in $(seq 128 256 10240); do
    dd if=/dev/urandom of=<disk> bs=128k count=4 skip=$i
done

The intent was to poke 512 KB of random data into the disk at a number of different places, with the goal of both hopefully overwriting space that was actually in use and not overwriting too much of it. This turned out to actually not do very much and I spent some time scratching my head before the penny dropped.

I've used skip before and honestly, I wasn't thinking clearly here. What I actually wanted to use was seek. The difference is this:

skip skips over initial data in the input, while seek skips over initial data in the output.

(Technically I think skip usually silently consumes the initial input data you asked it to skip over, although dd may try to lseek() on inputs that seem to support it. seek definitely must lseek() and dd will error out if you ask it to seek on something that doesn't support lseek(), like a pipe.)

What I was really doing with my dd command was throwing away increasing amounts of data from /dev/urandom and then repeatedly writing 512 KB (of random data) over the start of the disk. This was nowhere near what I intended and certainly didn't have the effects on ZFS that I wanted.

I guess the way for me to remember this is 'skip initial data from the input, seek over space in the output'. Hopefully it will stick after this experience in toe stubbing.

Sidebar: the other thing I initially did wrong

The test pool was full of test files, which I had created by copying /dev/zero into files. My initial dd was also using /dev/zero to overwrite disk blocks. It struck me that I was likely to be mostly overwriting file data blocks full of zeroes with more zeroes, which probably wasn't going to cause checksum failures.

DdSkipVersusSeek written at 22:34:17; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.