diff options
author | Nick Thomas <nick@gitlab.com> | 2018-04-25 18:25:48 +0300 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2018-04-27 19:23:39 +0300 |
commit | ca27f8df7263232c00f2cc0c476408feceb9c9f6 (patch) | |
tree | 356d953c908c1c834c794bd0d519bae99b5b2bf1 | |
parent | fef9c6546b3ce00234db349dac2eabce1ffd2b40 (diff) |
Create /dev/random and /dev/urandom when daemonizing and jailing
Go on Linux normally uses the getrandom() syscall to get entropy. However,
this is onl available for Linux version >= 3.17
Making /dev/random and /dev/urandom available in the chrooted jail allows the
daemon to operate correctly on these systems. It's important to handle this
automatically, as the previous workaround of manually creating these character
devices is made more difficult by the use of a random directory in $TMPDIR.
-rw-r--r-- | daemon.go | 11 | ||||
-rw-r--r-- | internal/jail/jail.go | 55 | ||||
-rw-r--r-- | internal/jail/jail_test.go | 37 |
3 files changed, 102 insertions, 1 deletions
@@ -110,6 +110,17 @@ func daemonChroot(cmd *exec.Cmd) (*jail.Jail, error) { return nil, err } + // Add /dev/urandom and /dev/random inside the jail. This is required to + // support Linux versions < 3.17, which do not have the getrandom() syscall + cage.MkDir("/dev", 0755) + if err := cage.CharDev("/dev/urandom"); err != nil { + return nil, err + } + + if err := cage.CharDev("/dev/random"); err != nil { + return nil, err + } + // Add gitlab-pages inside the jail err = cage.CopyTo("/gitlab-pages", cmd.Path) if err != nil { diff --git a/internal/jail/jail.go b/internal/jail/jail.go index 16690235..d2b04be0 100644 --- a/internal/jail/jail.go +++ b/internal/jail/jail.go @@ -5,12 +5,18 @@ import ( "io" "os" "path" + "syscall" "time" + + "golang.org/x/sys/unix" ) type pathAndMode struct { path string mode os.FileMode + + // Only respected if mode is os.ModeCharDevice + rdev int } // Jail is a Chroot jail builder @@ -64,7 +70,7 @@ func (j *Jail) Build() error { } for dest, src := range j.files { - if err := copyFile(dest, src.path, src.mode); err != nil { + if err := handleFile(dest, src); err != nil { j.removeAll() return fmt.Errorf("Can't copy %q -> %q. %s", src.path, dest, err) } @@ -102,6 +108,34 @@ func (j *Jail) MkDir(path string, perm os.FileMode) { j.directories = append(j.directories, pathAndMode{path: j.ExternalPath(path), mode: perm}) } +// CharDev enqueues an mknod operation for the given character device at jail +// building time +func (j *Jail) CharDev(path string) error { + fi, err := os.Stat(path) + if err != nil { + return fmt.Errorf("Can't stat %q: %s", path, err) + } + + if (fi.Mode() & os.ModeCharDevice) == 0 { + return fmt.Errorf("Can't mknod %q: not a character device", path) + } + + // Read the device number from the underlying unix implementation of stat() + sys, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("Couldn't determine rdev for %q", path) + } + + jailedDest := j.ExternalPath(path) + j.files[jailedDest] = pathAndMode{ + path: path, + mode: fi.Mode(), + rdev: int(sys.Rdev), + } + + return nil +} + // CopyTo enqueues a file copy operation at jail building time func (j *Jail) CopyTo(dest, src string) error { fi, err := os.Stat(src) @@ -143,6 +177,25 @@ func (j *Jail) ExternalPath(internal string) string { return path.Join(j.Path(), internal) } +func handleFile(dest string, src pathAndMode) error { + // Using `io.Copy` on a character device simply doesn't work + if (src.mode & os.ModeCharDevice) > 0 { + return createCharacterDevice(dest, src) + } + + // FIXME: currently, symlinks, block devices, named pipes and other + // non-regular files will be `Open`ed and have that content streamed to a + // regular file inside the chroot. This is actually desired behaviour for, + // e.g., `/etc/resolv.conf`, but was very surprising + return copyFile(dest, src.path, src.mode) +} + +func createCharacterDevice(dest string, src pathAndMode) error { + unixMode := uint32(src.mode.Perm() | syscall.S_IFCHR) + + return unix.Mknod(dest, unixMode, src.rdev) +} + func copyFile(dest, src string, perm os.FileMode) error { srcFile, err := os.Open(src) if err != nil { diff --git a/internal/jail/jail_test.go b/internal/jail/jail_test.go index 6c96ceeb..6de870df 100644 --- a/internal/jail/jail_test.go +++ b/internal/jail/jail_test.go @@ -6,6 +6,7 @@ import ( "os" "path" "runtime" + "syscall" "testing" "time" @@ -113,6 +114,42 @@ func TestJailDisposeDoNotFailOnMissingPath(t *testing.T) { assert.NoError(err) } +func TestJailWithCharacterDevice(t *testing.T) { + if os.Geteuid() != 0 { + t.Log("This test only works if run as root") + t.SkipNow() + } + + // Determine the expected rdev + fi, err := os.Stat("/dev/urandom") + require.NoError(t, err) + sys, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + t.Log("Couldn't determine expected rdev for /dev/urandom, skipping") + t.SkipNow() + } + + expectedRdev := sys.Rdev + + jailPath := tmpJailPath() + cage := jail.New(jailPath, 0755) + cage.MkDir("/dev", 0755) + + require.NoError(t, cage.CharDev("/dev/urandom")) + require.NoError(t, cage.Build()) + defer cage.Dispose() + + fi, err = os.Lstat(path.Join(cage.Path(), "/dev/urandom")) + require.NoError(t, err) + + isCharDev := fi.Mode()&os.ModeCharDevice == os.ModeCharDevice + assert.True(t, isCharDev, "Created file was not a character device") + + sys, ok = fi.Sys().(*syscall.Stat_t) + require.True(t, ok, "Couldn't determine rdev of created character device") + assert.Equal(t, expectedRdev, sys.Rdev, "Incorrect rdev for /dev/urandom") +} + func TestJailWithFiles(t *testing.T) { tests := []struct { name string |