diff options
Diffstat (limited to 'pkg/sentry')
-rw-r--r-- | pkg/sentry/fsimpl/verity/filesystem.go | 18 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/verity/verity.go | 66 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/verity/verity_test.go | 407 | ||||
-rw-r--r-- | pkg/sentry/vfs/vfs.go | 4 |
4 files changed, 470 insertions, 25 deletions
diff --git a/pkg/sentry/fsimpl/verity/filesystem.go b/pkg/sentry/fsimpl/verity/filesystem.go index a4ad625bb..9057d2b4e 100644 --- a/pkg/sentry/fsimpl/verity/filesystem.go +++ b/pkg/sentry/fsimpl/verity/filesystem.go @@ -426,6 +426,17 @@ func (fs *filesystem) verifyStatAndChildrenLocked(ctx context.Context, d *dentry params.DataAndTreeInSameFile = true } + if d.isSymlink() { + target, err := vfsObj.ReadlinkAt(ctx, d.fs.creds, &vfs.PathOperation{ + Root: d.lowerVD, + Start: d.lowerVD, + }) + if err != nil { + return err + } + params.SymlinkTarget = target + } + if _, err := merkletree.Verify(params); err != nil && err != io.EOF { return alertIntegrityViolation(fmt.Sprintf("Verification stat for %s failed: %v", childPath, err)) } @@ -433,6 +444,7 @@ func (fs *filesystem) verifyStatAndChildrenLocked(ctx context.Context, d *dentry d.uid = stat.UID d.gid = stat.GID d.size = uint32(size) + d.symlinkTarget = params.SymlinkTarget return nil } @@ -934,11 +946,7 @@ func (fs *filesystem) ReadlinkAt(ctx context.Context, rp *vfs.ResolvingPath) (st if err != nil { return "", err } - //TODO(b/162787271): Provide integrity check for ReadlinkAt. - return fs.vfsfs.VirtualFilesystem().ReadlinkAt(ctx, d.fs.creds, &vfs.PathOperation{ - Root: d.lowerVD, - Start: d.lowerVD, - }) + return d.readlink(ctx) } // RenameAt implements vfs.FilesystemImpl.RenameAt. diff --git a/pkg/sentry/fsimpl/verity/verity.go b/pkg/sentry/fsimpl/verity/verity.go index 8645078a0..374f71568 100644 --- a/pkg/sentry/fsimpl/verity/verity.go +++ b/pkg/sentry/fsimpl/verity/verity.go @@ -332,6 +332,11 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt d.hash = make([]byte, len(iopts.RootHash)) d.childrenNames = make(map[string]struct{}) + if !d.isDir() { + ctx.Warningf("verity root must be a directory") + return nil, nil, syserror.EINVAL + } + if !fs.allowRuntimeEnable { // Get children names from the underlying file system. offString, err := vfsObj.GetXattrAt(ctx, creds, &vfs.PathOperation{ @@ -461,6 +466,9 @@ type dentry struct { // initialized. lowerMerkleVD vfs.VirtualDentry + // symlinkTarget is the target path of a symlink file in the underlying filesystem. + symlinkTarget string + // hash is the calculated hash for the current file or directory. hash // is protected by hashMu. hashMu sync.RWMutex `state:"nosave"` @@ -645,6 +653,23 @@ func (d *dentry) getLowerAt(ctx context.Context, vfsObj *vfs.VirtualFilesystem, } func (d *dentry) readlink(ctx context.Context) (string, error) { + vfsObj := d.fs.vfsfs.VirtualFilesystem() + if d.verityEnabled() { + stat, err := vfsObj.StatAt(ctx, d.fs.creds, &vfs.PathOperation{ + Root: d.lowerVD, + Start: d.lowerVD, + }, &vfs.StatOptions{}) + if err != nil { + return "", err + } + d.dirMu.Lock() + defer d.dirMu.Unlock() + if err := d.fs.verifyStatAndChildrenLocked(ctx, d, stat); err != nil { + return "", err + } + return d.symlinkTarget, nil + } + return d.fs.vfsfs.VirtualFilesystem().ReadlinkAt(ctx, d.fs.creds, &vfs.PathOperation{ Root: d.lowerVD, Start: d.lowerVD, @@ -756,7 +781,8 @@ func (fd *fileDescription) Seek(ctx context.Context, offset int64, whence int32) // hash of the generated Merkle tree and the data size is returned. If fd // points to a regular file, the data is the content of the file. If fd points // to a directory, the data is all hashes of its children, written to the Merkle -// tree file. +// tree file. If fd represents a symlink, the data is empty and nothing is written +// to the Merkle tree file. // // Preconditions: fd.d.fs.verityMu must be locked. func (fd *fileDescription) generateMerkleLocked(ctx context.Context) ([]byte, uint64, error) { @@ -772,30 +798,30 @@ func (fd *fileDescription) generateMerkleLocked(ctx context.Context) ([]byte, ui FD: fd.merkleWriter, Ctx: ctx, } + + stat, err := fd.lowerFD.Stat(ctx, vfs.StatOptions{}) + if err != nil { + return nil, 0, err + } + params := &merkletree.GenerateParams{ TreeReader: &merkleReader, TreeWriter: &merkleWriter, Children: fd.d.childrenNames, //TODO(b/156980949): Support passing other hash algorithms. HashAlgorithms: fd.d.fs.alg.toLinuxHashAlg(), + Name: fd.d.name, + Mode: uint32(stat.Mode), + UID: stat.UID, + GID: stat.GID, } switch atomic.LoadUint32(&fd.d.mode) & linux.S_IFMT { case linux.S_IFREG: // For a regular file, generate a Merkle tree based on its // content. - var err error - stat, err := fd.lowerFD.Stat(ctx, vfs.StatOptions{}) - if err != nil { - return nil, 0, err - } - params.File = &fdReader params.Size = int64(stat.Size) - params.Name = fd.d.name - params.Mode = uint32(stat.Mode) - params.UID = stat.UID - params.GID = stat.GID params.DataAndTreeInSameFile = false case linux.S_IFDIR: // For a directory, generate a Merkle tree based on the hashes @@ -807,18 +833,20 @@ func (fd *fileDescription) generateMerkleLocked(ctx context.Context) ([]byte, ui } params.Size = int64(merkleStat.Size) - - stat, err := fd.lowerFD.Stat(ctx, vfs.StatOptions{}) + params.File = &merkleReader + params.DataAndTreeInSameFile = true + case linux.S_IFLNK: + // For a symlink, generate a Merkle tree file but do not write the root hash + // of the target file content to it. Return a hash of a VerityDescriptor object + // which includes the symlink target name. + target, err := fd.d.readlink(ctx) if err != nil { return nil, 0, err } - params.File = &merkleReader - params.Name = fd.d.name - params.Mode = uint32(stat.Mode) - params.UID = stat.UID - params.GID = stat.GID - params.DataAndTreeInSameFile = true + params.Size = int64(stat.Size) + params.DataAndTreeInSameFile = false + params.SymlinkTarget = target default: // TODO(b/167728857): Investigate whether and how we should // enable other types of file. diff --git a/pkg/sentry/fsimpl/verity/verity_test.go b/pkg/sentry/fsimpl/verity/verity_test.go index 798d6a9bd..57bd65202 100644 --- a/pkg/sentry/fsimpl/verity/verity_test.go +++ b/pkg/sentry/fsimpl/verity/verity_test.go @@ -163,6 +163,17 @@ func (d *dentry) openLowerMerkleAt(ctx context.Context, vfsObj *vfs.VirtualFiles }) } +// mkdirLowerAt creates a directory in the underlying file system. +func (d *dentry) mkdirLowerAt(ctx context.Context, vfsObj *vfs.VirtualFilesystem, path string, mode linux.FileMode) error { + return vfsObj.MkdirAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: d.lowerVD, + Start: d.lowerVD, + Path: fspath.Parse(path), + }, &vfs.MkdirOptions{ + Mode: mode, + }) +} + // unlinkLowerAt deletes the file in the underlying file system. func (d *dentry) unlinkLowerAt(ctx context.Context, vfsObj *vfs.VirtualFilesystem, path string) error { return vfsObj.UnlinkAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ @@ -208,6 +219,16 @@ func (d *dentry) renameLowerMerkleAt(ctx context.Context, vfsObj *vfs.VirtualFil }, &vfs.RenameOptions{}) } +// symlinkLowerAt creates a symbolic link at symlink referring to the given target +// in the underlying filesystem. +func (d *dentry) symlinkLowerAt(ctx context.Context, vfsObj *vfs.VirtualFilesystem, target, symlink string) error { + return vfsObj.SymlinkAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: d.lowerVD, + Start: d.lowerVD, + Path: fspath.Parse(symlink), + }, target) +} + // newFileFD creates a new file in the verity mount, and returns the FD. The FD // points to a file that has random data generated. func newFileFD(ctx context.Context, t *testing.T, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry, filePath string, mode linux.FileMode) (*vfs.FileDescription, int, error) { @@ -239,6 +260,18 @@ func newFileFD(ctx context.Context, t *testing.T, vfsObj *vfs.VirtualFilesystem, return fd, dataSize, err } +// newDirFD creates a new directory in the verity mount, and returns the FD. +func newDirFD(ctx context.Context, t *testing.T, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry, dirPath string, mode linux.FileMode) (*vfs.FileDescription, error) { + // Create the directory in the underlying file system. + if err := dentryFromVD(t, root).mkdirLowerAt(ctx, vfsObj, dirPath, linux.ModeRegular|mode); err != nil { + return nil, err + } + if _, err := dentryFromVD(t, root).openLowerAt(ctx, vfsObj, dirPath, linux.O_RDONLY|linux.O_DIRECTORY, linux.ModeRegular|mode); err != nil { + return nil, err + } + return openVerityAt(ctx, vfsObj, root, dirPath, linux.O_RDONLY|linux.O_DIRECTORY, mode) +} + // newEmptyFileFD creates a new empty file in the verity mount, and returns the FD. func newEmptyFileFD(ctx context.Context, t *testing.T, vfsObj *vfs.VirtualFilesystem, root vfs.VirtualDentry, filePath string, mode linux.FileMode) (*vfs.FileDescription, error) { // Create the file in the underlying file system. @@ -801,3 +834,377 @@ func TestOpenRenamedFileFails(t *testing.T) { }) } } + +// TestUnmodifiedSymlinkFileReadSucceeds ensures that readlink() for an +// unmodified verity enabled symlink succeeds. +func TestUnmodifiedSymlinkFileReadSucceeds(t *testing.T) { + testCases := []struct { + name string + // The symlink target is a directory. + hasDirectoryTarget bool + // The symlink target is a directory and contains a regular file which will be + // used to test walking a symlink. + testWalk bool + }{ + { + name: "RegularFileTarget", + hasDirectoryTarget: false, + testWalk: false, + }, + { + name: "DirectoryTarget", + hasDirectoryTarget: true, + testWalk: false, + }, + { + name: "RegularFileInSymlinkDirectory", + hasDirectoryTarget: true, + testWalk: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.testWalk && !tc.hasDirectoryTarget { + t.Fatalf("Invalid test case: hasDirectoryTarget can't be false when testing symlink walk") + } + + vfsObj, root, ctx, err := newVerityRoot(t, SHA256) + if err != nil { + t.Fatalf("newVerityRoot: %v", err) + } + + var target string + if tc.hasDirectoryTarget { + target = "verity-test-dir" + if _, err := newDirFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newDirFD: %v", err) + } + } else { + target = "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + if tc.testWalk { + fileInTargetDirectory := target + "/" + "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, fileInTargetDirectory, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + symlink := "verity-test-symlink" + if err := dentryFromVD(t, root).symlinkLowerAt(ctx, vfsObj, target, symlink); err != nil { + t.Fatalf("SymlinkAt: %v", err) + } + + fd, err := openVerityAt(ctx, vfsObj, root, symlink, linux.O_PATH|linux.O_NOFOLLOW, linux.ModeRegular) + + if err != nil { + t.Fatalf("openVerityAt symlink: %v", err) + } + + enableVerity(ctx, t, fd) + + if _, err := vfsObj.ReadlinkAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse(symlink), + }); err != nil { + t.Fatalf("ReadlinkAt: %v", err) + } + + if tc.testWalk { + fileInSymlinkDirectory := symlink + "/verity-test-file" + // Ensure opening the verity enabled file in the symlink directory succeeds. + if _, err := openVerityAt(ctx, vfsObj, root, fileInSymlinkDirectory, linux.O_RDONLY, linux.ModeRegular); err != nil { + t.Errorf("open enabled file failed: %v", err) + } + } + }) + } +} + +// TestDeletedSymlinkFileReadFails ensures that reading value of a deleted verity enabled +// symlink fails. +func TestDeletedSymlinkFileReadFails(t *testing.T) { + testCases := []struct { + name string + // The original symlink is unlinked if deleteLink is true. + deleteLink bool + // The Merkle tree file is renamed if deleteMerkleFile is true. + deleteMerkleFile bool + // The symlink target is a directory. + hasDirectoryTarget bool + // The symlink target is a directory and contains a regular file which will be + // used to test walking a symlink. + testWalk bool + }{ + { + name: "DeleteLinkRegularFile", + deleteLink: true, + deleteMerkleFile: false, + hasDirectoryTarget: false, + testWalk: false, + }, + { + name: "DeleteMerkleRegFile", + deleteLink: false, + deleteMerkleFile: true, + hasDirectoryTarget: false, + testWalk: false, + }, + { + name: "DeleteLinkAndMerkleRegFile", + deleteLink: true, + deleteMerkleFile: true, + hasDirectoryTarget: false, + testWalk: false, + }, + { + name: "DeleteLinkDirectory", + deleteLink: true, + deleteMerkleFile: false, + hasDirectoryTarget: true, + testWalk: false, + }, + { + name: "DeleteMerkleDirectory", + deleteLink: false, + deleteMerkleFile: true, + hasDirectoryTarget: true, + testWalk: false, + }, + { + name: "DeleteLinkAndMerkleDirectory", + deleteLink: true, + deleteMerkleFile: true, + hasDirectoryTarget: true, + testWalk: false, + }, + { + name: "DeleteLinkDirectoryWalk", + deleteLink: true, + deleteMerkleFile: false, + hasDirectoryTarget: true, + testWalk: true, + }, + { + name: "DeleteMerkleDirectoryWalk", + deleteLink: false, + deleteMerkleFile: true, + hasDirectoryTarget: true, + testWalk: true, + }, + { + name: "DeleteLinkAndMerkleDirectoryWalk", + deleteLink: true, + deleteMerkleFile: true, + hasDirectoryTarget: true, + testWalk: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.testWalk && !tc.hasDirectoryTarget { + t.Fatalf("Invalid test case: hasDirectoryTarget can't be false when testing symlink walk") + } + + vfsObj, root, ctx, err := newVerityRoot(t, SHA256) + if err != nil { + t.Fatalf("newVerityRoot: %v", err) + } + + var target string + if tc.hasDirectoryTarget { + target = "verity-test-dir" + if _, err := newDirFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newDirFD: %v", err) + } + } else { + target = "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + symlink := "verity-test-symlink" + if err := dentryFromVD(t, root).symlinkLowerAt(ctx, vfsObj, target, symlink); err != nil { + t.Fatalf("SymlinkAt: %v", err) + } + + fd, err := openVerityAt(ctx, vfsObj, root, symlink, linux.O_PATH|linux.O_NOFOLLOW, linux.ModeRegular) + + if err != nil { + t.Fatalf("openVerityAt symlink: %v", err) + } + + if tc.testWalk { + fileInTargetDirectory := target + "/" + "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, fileInTargetDirectory, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + enableVerity(ctx, t, fd) + + if tc.deleteLink { + if err := dentryFromVD(t, root).unlinkLowerAt(ctx, vfsObj, symlink); err != nil { + t.Fatalf("UnlinkAt: %v", err) + } + } + if tc.deleteMerkleFile { + if err := dentryFromVD(t, root).unlinkLowerMerkleAt(ctx, vfsObj, symlink); err != nil { + t.Fatalf("UnlinkAt: %v", err) + } + } + if _, err := vfsObj.ReadlinkAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse(symlink), + }); err != syserror.EIO { + t.Fatalf("ReadlinkAt succeeded with modified symlink: %v", err) + } + + if tc.testWalk { + fileInSymlinkDirectory := symlink + "/verity-test-file" + // Ensure opening the verity enabled file in the symlink directory fails. + if _, err := openVerityAt(ctx, vfsObj, root, fileInSymlinkDirectory, linux.O_RDONLY, linux.ModeRegular); err != syserror.EIO { + t.Errorf("Open succeeded with modified symlink: %v", err) + } + } + }) + } +} + +// TestModifiedSymlinkFileReadFails ensures that reading value of a modified verity enabled +// symlink fails. +func TestModifiedSymlinkFileReadFails(t *testing.T) { + testCases := []struct { + name string + // The symlink target is a directory. + hasDirectoryTarget bool + // The symlink target is a directory and contains a regular file which will be + // used to test walking a symlink. + testWalk bool + }{ + { + name: "RegularFileTarget", + hasDirectoryTarget: false, + testWalk: false, + }, + { + name: "DirectoryTarget", + hasDirectoryTarget: true, + testWalk: false, + }, + { + name: "RegularFileInSymlinkDirectory", + hasDirectoryTarget: true, + testWalk: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.testWalk && !tc.hasDirectoryTarget { + t.Fatalf("Invalid test case: hasDirectoryTarget can't be false when testing symlink walk") + } + + vfsObj, root, ctx, err := newVerityRoot(t, SHA256) + if err != nil { + t.Fatalf("newVerityRoot: %v", err) + } + + var target string + if tc.hasDirectoryTarget { + target = "verity-test-dir" + if _, err := newDirFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newDirFD: %v", err) + } + } else { + target = "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, target, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + // Create symlink which points to target file. + symlink := "verity-test-symlink" + if err := dentryFromVD(t, root).symlinkLowerAt(ctx, vfsObj, target, symlink); err != nil { + t.Fatalf("SymlinkAt: %v", err) + } + + // Open symlink file to get the fd for ioctl in new step. + fd, err := openVerityAt(ctx, vfsObj, root, symlink, linux.O_PATH|linux.O_NOFOLLOW, linux.ModeRegular) + if err != nil { + t.Fatalf("OpenAt symlink: %v", err) + } + + if tc.testWalk { + fileInTargetDirectory := target + "/" + "verity-test-file" + if _, _, err := newFileFD(ctx, t, vfsObj, root, fileInTargetDirectory, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + enableVerity(ctx, t, fd) + + var newTarget string + if tc.hasDirectoryTarget { + newTarget = "verity-test-dir-new" + if _, err := newDirFD(ctx, t, vfsObj, root, newTarget, 0644); err != nil { + t.Fatalf("newDirFD: %v", err) + } + } else { + newTarget = "verity-test-file-new" + if _, _, err := newFileFD(ctx, t, vfsObj, root, newTarget, 0644); err != nil { + t.Fatalf("newFileFD: %v", err) + } + } + + // Unlink symlink->target. + if err := dentryFromVD(t, root).unlinkLowerAt(ctx, vfsObj, symlink); err != nil { + t.Fatalf("UnlinkAt: %v", err) + } + + // Link symlink->newTarget. + if err := dentryFromVD(t, root).symlinkLowerAt(ctx, vfsObj, newTarget, symlink); err != nil { + t.Fatalf("SymlinkAt: %v", err) + } + + // Freshen lower dentry for symlink. + symlinkVD, err := vfsObj.GetDentryAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse(symlink), + }, &vfs.GetDentryOptions{}) + if err != nil { + t.Fatalf("Failed to get symlink dentry: %v", err) + } + symlinkDentry := dentryFromVD(t, symlinkVD) + + symlinkLowerVD, err := dentryFromVD(t, root).getLowerAt(ctx, vfsObj, symlink) + if err != nil { + t.Fatalf("Failed to get symlink lower dentry: %v", err) + } + symlinkDentry.lowerVD = symlinkLowerVD + + // Verify ReadlinkAt() fails. + if _, err := vfsObj.ReadlinkAt(ctx, auth.CredentialsFromContext(ctx), &vfs.PathOperation{ + Root: root, + Start: root, + Path: fspath.Parse(symlink), + }); err != syserror.EIO { + t.Fatalf("ReadlinkAt succeeded with modified symlink: %v", err) + } + + if tc.testWalk { + fileInSymlinkDirectory := symlink + "/verity-test-file" + // Ensure opening the verity enabled file in the symlink directory fails. + if _, err := openVerityAt(ctx, vfsObj, root, fileInSymlinkDirectory, linux.O_RDONLY, linux.ModeRegular); err != syserror.EIO { + t.Errorf("Open succeeded with modified symlink: %v", err) + } + } + }) + } +} diff --git a/pkg/sentry/vfs/vfs.go b/pkg/sentry/vfs/vfs.go index 0aff2dd92..b0e13cdab 100644 --- a/pkg/sentry/vfs/vfs.go +++ b/pkg/sentry/vfs/vfs.go @@ -425,7 +425,9 @@ func (vfs *VirtualFilesystem) OpenAt(ctx context.Context, creds *auth.Credential rp.mustBeDir = true rp.mustBeDirOrig = true } - if opts.Flags&linux.O_PATH != 0 { + // Ignore O_PATH for verity, as verity performs extra operations on the fd for verification. + // The underlying filesystem that verity wraps opens the fd with O_PATH. + if opts.Flags&linux.O_PATH != 0 && rp.mount.fs.FilesystemType().Name() != "verity" { vd, err := vfs.GetDentryAt(ctx, creds, pop, &GetDentryOptions{}) if err != nil { return nil, err |