mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Implement PRead for verity fs
PRead is implemented by read from the underlying file in blocks, and verify each block. The verified contents are saved into the output buffer. PiperOrigin-RevId: 332092267
This commit is contained in:
@@ -225,9 +225,9 @@ func Generate(data io.ReadSeeker, dataSize int64, treeReader io.ReadSeeker, tree
|
||||
// Verify will modify the cursor for data, but always restores it to its
|
||||
// original position upon exit. The cursor for tree is modified and not
|
||||
// restored.
|
||||
func Verify(w io.Writer, data, tree io.ReadSeeker, dataSize int64, readOffset int64, readSize int64, expectedRoot []byte, dataAndTreeInSameFile bool) error {
|
||||
func Verify(w io.Writer, data, tree io.ReadSeeker, dataSize int64, readOffset int64, readSize int64, expectedRoot []byte, dataAndTreeInSameFile bool) (int64, error) {
|
||||
if readSize <= 0 {
|
||||
return fmt.Errorf("Unexpected read size: %d", readSize)
|
||||
return 0, fmt.Errorf("Unexpected read size: %d", readSize)
|
||||
}
|
||||
layout := InitLayout(int64(dataSize), dataAndTreeInSameFile)
|
||||
|
||||
@@ -240,29 +240,30 @@ func Verify(w io.Writer, data, tree io.ReadSeeker, dataSize int64, readOffset in
|
||||
// finishes.
|
||||
origOffset, err := data.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Find current data offset failed: %v", err)
|
||||
return 0, fmt.Errorf("Find current data offset failed: %v", err)
|
||||
}
|
||||
defer data.Seek(origOffset, io.SeekStart)
|
||||
|
||||
// Move to the first block that contains target data.
|
||||
if _, err := data.Seek(firstDataBlock*layout.blockSize, io.SeekStart); err != nil {
|
||||
return fmt.Errorf("Seek to datablock start failed: %v", err)
|
||||
return 0, fmt.Errorf("Seek to datablock start failed: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, layout.blockSize)
|
||||
var readErr error
|
||||
bytesRead := 0
|
||||
total := int64(0)
|
||||
for i := firstDataBlock; i <= lastDataBlock; i++ {
|
||||
// Read a block that includes all or part of target range in
|
||||
// input data.
|
||||
bytesRead, readErr = data.Read(buf)
|
||||
bytesRead, err := data.Read(buf)
|
||||
readErr = err
|
||||
// If at the end of input data and all previous blocks are
|
||||
// verified, return the verified input data and EOF.
|
||||
if readErr == io.EOF && bytesRead == 0 {
|
||||
break
|
||||
}
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return fmt.Errorf("Read from data failed: %v", err)
|
||||
return 0, fmt.Errorf("Read from data failed: %v", err)
|
||||
}
|
||||
// If this is the end of file, zero the remaining bytes in buf,
|
||||
// otherwise they are still from the previous block.
|
||||
@@ -274,7 +275,7 @@ func Verify(w io.Writer, data, tree io.ReadSeeker, dataSize int64, readOffset in
|
||||
}
|
||||
}
|
||||
if err := verifyBlock(tree, layout, buf, i, expectedRoot); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
// startOff is the beginning of the read range within the
|
||||
// current data block. Note that for all blocks other than the
|
||||
@@ -298,10 +299,14 @@ func Verify(w io.Writer, data, tree io.ReadSeeker, dataSize int64, readOffset in
|
||||
if endOff > int64(bytesRead) {
|
||||
endOff = int64(bytesRead)
|
||||
}
|
||||
w.Write(buf[startOff:endOff])
|
||||
n, err := w.Write(buf[startOff:endOff])
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
total += int64(n)
|
||||
|
||||
}
|
||||
return readErr
|
||||
return total, readErr
|
||||
}
|
||||
|
||||
// verifyBlock verifies a block against tree. index is the number of block in
|
||||
|
||||
@@ -67,17 +67,17 @@ func TestLayout(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%d", tc.dataSize), func(t *testing.T) {
|
||||
l := InitLayout(tc.dataSize, tc.dataAndTreeInSameFile)
|
||||
if l.blockSize != int64(usermem.PageSize) {
|
||||
t.Errorf("got blockSize %d, want %d", l.blockSize, usermem.PageSize)
|
||||
t.Errorf("Got blockSize %d, want %d", l.blockSize, usermem.PageSize)
|
||||
}
|
||||
if l.digestSize != sha256DigestSize {
|
||||
t.Errorf("got digestSize %d, want %d", l.digestSize, sha256DigestSize)
|
||||
t.Errorf("Got digestSize %d, want %d", l.digestSize, sha256DigestSize)
|
||||
}
|
||||
if l.numLevels() != len(tc.expectedLevelOffset) {
|
||||
t.Errorf("got levels %d, want %d", l.numLevels(), len(tc.expectedLevelOffset))
|
||||
t.Errorf("Got levels %d, want %d", l.numLevels(), len(tc.expectedLevelOffset))
|
||||
}
|
||||
for i := 0; i < l.numLevels() && i < len(tc.expectedLevelOffset); i++ {
|
||||
if l.levelOffset[i] != tc.expectedLevelOffset[i] {
|
||||
t.Errorf("got levelStart[%d] %d, want %d", i, l.levelOffset[i], tc.expectedLevelOffset[i])
|
||||
t.Errorf("Got levelStart[%d] %d, want %d", i, l.levelOffset[i], tc.expectedLevelOffset[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -169,11 +169,11 @@ func TestGenerate(t *testing.T) {
|
||||
}, int64(len(tc.data)), &tree, &tree, dataAndTreeInSameFile)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("got err: %v, want nil", err)
|
||||
t.Fatalf("Got err: %v, want nil", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(root, tc.expectedRoot) {
|
||||
t.Errorf("got root: %v, want %v", root, tc.expectedRoot)
|
||||
t.Errorf("Got root: %v, want %v", root, tc.expectedRoot)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -334,14 +334,21 @@ func TestVerify(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
data[tc.modifyByte] ^= 1
|
||||
if tc.shouldSucceed {
|
||||
if err := Verify(&buf, bytes.NewReader(data), &tree, tc.dataSize, tc.verifyStart, tc.verifySize, root, dataAndTreeInSameFile); err != nil && err != io.EOF {
|
||||
n, err := Verify(&buf, bytes.NewReader(data), &tree, tc.dataSize, tc.verifyStart, tc.verifySize, root, dataAndTreeInSameFile)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Errorf("Verification failed when expected to succeed: %v", err)
|
||||
}
|
||||
if int64(buf.Len()) != tc.verifySize || !bytes.Equal(data[tc.verifyStart:tc.verifyStart+tc.verifySize], buf.Bytes()) {
|
||||
t.Errorf("Incorrect output from Verify")
|
||||
if n != tc.verifySize {
|
||||
t.Errorf("Got Verify output size %d, want %d", n, tc.verifySize)
|
||||
}
|
||||
if int64(buf.Len()) != tc.verifySize {
|
||||
t.Errorf("Got Verify output buf size %d, want %d,", buf.Len(), tc.verifySize)
|
||||
}
|
||||
if !bytes.Equal(data[tc.verifyStart:tc.verifyStart+tc.verifySize], buf.Bytes()) {
|
||||
t.Errorf("Incorrect output buf from Verify")
|
||||
}
|
||||
} else {
|
||||
if err := Verify(&buf, bytes.NewReader(data), &tree, tc.dataSize, tc.verifyStart, tc.verifySize, root, dataAndTreeInSameFile); err == nil {
|
||||
if _, err := Verify(&buf, bytes.NewReader(data), &tree, tc.dataSize, tc.verifyStart, tc.verifySize, root, dataAndTreeInSameFile); err == nil {
|
||||
t.Errorf("Verification succeeded when expected to fail")
|
||||
}
|
||||
}
|
||||
@@ -382,14 +389,21 @@ func TestVerifyRandom(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Checks that the random portion of data from the original data is
|
||||
// verified successfully.
|
||||
if err := Verify(&buf, bytes.NewReader(data), &tree, dataSize, start, size, root, dataAndTreeInSameFile); err != nil && err != io.EOF {
|
||||
n, err := Verify(&buf, bytes.NewReader(data), &tree, dataSize, start, size, root, dataAndTreeInSameFile)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Errorf("Verification failed for correct data: %v", err)
|
||||
}
|
||||
if size > dataSize-start {
|
||||
size = dataSize - start
|
||||
}
|
||||
if int64(buf.Len()) != size || !bytes.Equal(data[start:start+size], buf.Bytes()) {
|
||||
t.Errorf("Incorrect output from Verify")
|
||||
if n != size {
|
||||
t.Errorf("Got Verify output size %d, want %d", n, size)
|
||||
}
|
||||
if int64(buf.Len()) != size {
|
||||
t.Errorf("Got Verify output buf size %d, want %d", buf.Len(), size)
|
||||
}
|
||||
if !bytes.Equal(data[start:start+size], buf.Bytes()) {
|
||||
t.Errorf("Incorrect output buf from Verify")
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
@@ -397,7 +411,7 @@ func TestVerifyRandom(t *testing.T) {
|
||||
randBytePos := rand.Int63n(size)
|
||||
data[start+randBytePos] ^= 1
|
||||
|
||||
if err := Verify(&buf, bytes.NewReader(data), &tree, dataSize, start, size, root, dataAndTreeInSameFile); err == nil {
|
||||
if _, err := Verify(&buf, bytes.NewReader(data), &tree, dataSize, start, size, root, dataAndTreeInSameFile); err == nil {
|
||||
t.Errorf("Verification succeeded for modified data")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +192,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// contains the expected xattrs. If the file or the xattr does not
|
||||
// exist, it indicates unexpected modifications to the file system.
|
||||
if err == syserror.ENOENT || err == syserror.ENODATA {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, err
|
||||
}
|
||||
panic(fmt.Sprintf("Failed to get xattr %s for %s: %v", merkleOffsetInParentXattr, childPath, err))
|
||||
return nil, alertIntegrityViolation(err, fmt.Sprintf("Failed to get xattr %s for %s: %v", merkleOffsetInParentXattr, childPath, err))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -204,10 +201,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// unexpected modifications to the file system.
|
||||
offset, err := strconv.Atoi(off)
|
||||
if err != nil {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, syserror.EINVAL
|
||||
}
|
||||
panic(fmt.Sprintf("Failed to convert xattr %s for %s to int: %v", merkleOffsetInParentXattr, childPath, err))
|
||||
return nil, alertIntegrityViolation(err, fmt.Sprintf("Failed to convert xattr %s for %s to int: %v", merkleOffsetInParentXattr, childPath, err))
|
||||
}
|
||||
|
||||
// Open parent Merkle tree file to read and verify child's root hash.
|
||||
@@ -221,10 +215,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// The parent Merkle tree file should have been created. If it's
|
||||
// missing, it indicates an unexpected modification to the file system.
|
||||
if err == syserror.ENOENT {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, err
|
||||
}
|
||||
panic(fmt.Sprintf("Failed to open parent Merkle file for %s: %v", childPath, err))
|
||||
return nil, alertIntegrityViolation(err, fmt.Sprintf("Failed to open parent Merkle file for %s: %v", childPath, err))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -242,10 +233,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// contains the expected xattrs. If the file or the xattr does not
|
||||
// exist, it indicates unexpected modifications to the file system.
|
||||
if err == syserror.ENOENT || err == syserror.ENODATA {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, err
|
||||
}
|
||||
panic(fmt.Sprintf("Failed to get xattr %s for %s: %v", merkleSizeXattr, childPath, err))
|
||||
return nil, alertIntegrityViolation(err, fmt.Sprintf("Failed to get xattr %s for %s: %v", merkleSizeXattr, childPath, err))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -255,10 +243,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// unexpected modifications to the file system.
|
||||
parentSize, err := strconv.Atoi(dataSize)
|
||||
if err != nil {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, syserror.EINVAL
|
||||
}
|
||||
panic(fmt.Sprintf("Failed to convert xattr %s for %s to int: %v", merkleSizeXattr, childPath, err))
|
||||
return nil, alertIntegrityViolation(syserror.EINVAL, fmt.Sprintf("Failed to convert xattr %s for %s to int: %v", merkleSizeXattr, childPath, err))
|
||||
}
|
||||
|
||||
fdReader := vfs.FileReadWriteSeeker{
|
||||
@@ -270,11 +255,8 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de
|
||||
// contain the root hash of the children in the parent Merkle tree when
|
||||
// Verify returns with success.
|
||||
var buf bytes.Buffer
|
||||
if err := merkletree.Verify(&buf, &fdReader, &fdReader, int64(parentSize), int64(offset), int64(merkletree.DigestSize()), parent.rootHash, true /* dataAndTreeInSameFile */); err != nil && err != io.EOF {
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, syserror.EIO
|
||||
}
|
||||
panic(fmt.Sprintf("Verification for %s failed: %v", childPath, err))
|
||||
if _, err := merkletree.Verify(&buf, &fdReader, &fdReader, int64(parentSize), int64(offset), int64(merkletree.DigestSize()), parent.rootHash, true /* dataAndTreeInSameFile */); err != nil && err != io.EOF {
|
||||
return nil, alertIntegrityViolation(syserror.EIO, fmt.Sprintf("Verification for %s failed: %v", childPath, err))
|
||||
}
|
||||
|
||||
// Cache child root hash when it's verified the first time.
|
||||
@@ -370,10 +352,7 @@ func (fs *filesystem) lookupAndVerifyLocked(ctx context.Context, parent *dentry,
|
||||
// corresponding Merkle tree is found. This indicates an
|
||||
// unexpected modification to the file system that
|
||||
// removed/renamed the child.
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, childErr
|
||||
}
|
||||
panic(fmt.Sprintf("Target file %s is expected but missing", parentPath+"/"+name))
|
||||
return nil, alertIntegrityViolation(childErr, fmt.Sprintf("Target file %s is expected but missing", parentPath+"/"+name))
|
||||
} else if childErr == nil && childMerkleErr == syserror.ENOENT {
|
||||
// If in allowRuntimeEnable mode, and the Merkle tree file is
|
||||
// not created yet, we create an empty Merkle tree file, so that
|
||||
@@ -409,10 +388,7 @@ func (fs *filesystem) lookupAndVerifyLocked(ctx context.Context, parent *dentry,
|
||||
// If runtime enable is not allowed. This indicates an
|
||||
// unexpected modification to the file system that
|
||||
// removed/renamed the Merkle tree file.
|
||||
if noCrashOnVerificationFailure {
|
||||
return nil, childMerkleErr
|
||||
}
|
||||
panic(fmt.Sprintf("Expected Merkle file for target %s but none found", parentPath+"/"+name))
|
||||
return nil, alertIntegrityViolation(childMerkleErr, fmt.Sprintf("Expected Merkle file for target %s but none found", parentPath+"/"+name))
|
||||
}
|
||||
} else if childErr == syserror.ENOENT && childMerkleErr == syserror.ENOENT {
|
||||
// Both the child and the corresponding Merkle tree are missing.
|
||||
@@ -421,7 +397,7 @@ func (fs *filesystem) lookupAndVerifyLocked(ctx context.Context, parent *dentry,
|
||||
// TODO(b/167752508): Investigate possible ways to differentiate
|
||||
// cases that both files are deleted from cases that they never
|
||||
// exist in the file system.
|
||||
panic(fmt.Sprintf("Failed to find file %s", parentPath+"/"+name))
|
||||
return nil, alertIntegrityViolation(childErr, fmt.Sprintf("Failed to find file %s", parentPath+"/"+name))
|
||||
}
|
||||
|
||||
mask := uint32(linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_UID | linux.STATX_GID)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
package verity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -29,7 +30,6 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
"gvisor.dev/gvisor/pkg/fspath"
|
||||
"gvisor.dev/gvisor/pkg/marshal/primitive"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/merkletree"
|
||||
"gvisor.dev/gvisor/pkg/sentry/arch"
|
||||
fslock "gvisor.dev/gvisor/pkg/sentry/fs/lock"
|
||||
@@ -135,6 +135,16 @@ func (FilesystemType) Name() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
// alertIntegrityViolation alerts a violation of integrity, which usually means
|
||||
// unexpected modification to the file system is detected. In
|
||||
// noCrashOnVerificationFailure mode, it returns an error, otherwise it panic.
|
||||
func alertIntegrityViolation(err error, msg string) error {
|
||||
if noCrashOnVerificationFailure {
|
||||
return err
|
||||
}
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
// GetFilesystem implements vfs.FilesystemType.GetFilesystem.
|
||||
func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.VirtualFilesystem, creds *auth.Credentials, source string, opts vfs.GetFilesystemOptions) (*vfs.Filesystem, *vfs.Dentry, error) {
|
||||
iopts, ok := opts.InternalData.(InternalFilesystemOptions)
|
||||
@@ -204,15 +214,12 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
|
||||
return nil, nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
// Failed to get dentry for the root Merkle file. This indicates
|
||||
// an attack that removed/renamed the root Merkle file, or it's
|
||||
// never generated.
|
||||
if noCrashOnVerificationFailure {
|
||||
fs.vfsfs.DecRef(ctx)
|
||||
d.DecRef(ctx)
|
||||
return nil, nil, err
|
||||
}
|
||||
panic("Failed to find root Merkle file")
|
||||
// Failed to get dentry for the root Merkle file. This
|
||||
// indicates an unexpected modification that removed/renamed
|
||||
// the root Merkle file, or it's never generated.
|
||||
fs.vfsfs.DecRef(ctx)
|
||||
d.DecRef(ctx)
|
||||
return nil, nil, alertIntegrityViolation(err, "Failed to find root Merkle file")
|
||||
}
|
||||
d.lowerMerkleVD = lowerMerkleVD
|
||||
|
||||
@@ -618,6 +625,54 @@ func (fd *fileDescription) Ioctl(ctx context.Context, uio usermem.IO, args arch.
|
||||
}
|
||||
}
|
||||
|
||||
// PRead implements vfs.FileDescriptionImpl.PRead.
|
||||
func (fd *fileDescription) PRead(ctx context.Context, dst usermem.IOSequence, offset int64, opts vfs.ReadOptions) (int64, error) {
|
||||
// No need to verify if the file is not enabled yet in
|
||||
// allowRuntimeEnable mode.
|
||||
if fd.d.fs.allowRuntimeEnable && len(fd.d.rootHash) == 0 {
|
||||
return fd.lowerFD.PRead(ctx, dst, offset, opts)
|
||||
}
|
||||
|
||||
// dataSize is the size of the whole file.
|
||||
dataSize, err := fd.merkleReader.GetXattr(ctx, &vfs.GetXattrOptions{
|
||||
Name: merkleSizeXattr,
|
||||
Size: sizeOfInt32,
|
||||
})
|
||||
|
||||
// The Merkle tree file for the child should have been created and
|
||||
// contains the expected xattrs. If the xattr does not exist, it
|
||||
// indicates unexpected modifications to the file system.
|
||||
if err == syserror.ENODATA {
|
||||
return 0, alertIntegrityViolation(err, fmt.Sprintf("Failed to get xattr %s: %v", merkleSizeXattr, err))
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// The dataSize xattr should be an integer. If it's not, it indicates
|
||||
// unexpected modifications to the file system.
|
||||
size, err := strconv.Atoi(dataSize)
|
||||
if err != nil {
|
||||
return 0, alertIntegrityViolation(err, fmt.Sprintf("Failed to convert xattr %s to int: %v", merkleSizeXattr, err))
|
||||
}
|
||||
|
||||
dataReader := vfs.FileReadWriteSeeker{
|
||||
FD: fd.lowerFD,
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
merkleReader := vfs.FileReadWriteSeeker{
|
||||
FD: fd.merkleReader,
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
n, err := merkletree.Verify(dst.Writer(ctx), &dataReader, &merkleReader, int64(size), offset, dst.NumBytes(), fd.d.rootHash, false /* dataAndTreeInSameFile */)
|
||||
if err != nil {
|
||||
return 0, alertIntegrityViolation(syserror.EINVAL, fmt.Sprintf("Verification failed: %v", err))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// LockPOSIX implements vfs.FileDescriptionImpl.LockPOSIX.
|
||||
func (fd *fileDescription) LockPOSIX(ctx context.Context, uid fslock.UniqueID, t fslock.LockType, start, length uint64, whence int16, block fslock.Blocker) error {
|
||||
return fd.Locks().LockPOSIX(ctx, &fd.vfsfd, uid, t, start, length, whence, block)
|
||||
|
||||
Reference in New Issue
Block a user