diff options
author | Chong Cai <chongc@google.com> | 2020-10-12 17:28:20 -0700 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2020-10-12 17:30:14 -0700 |
commit | ef90fe173380a8d769c699aec08737ef56f43c3e (patch) | |
tree | 5ef76cba87b92bb950427976b5b7cea158ef2418 /pkg | |
parent | e7bbe70f79aa9308c2eb54b057ee5779b22f478e (diff) |
Change Merkle tree library to use ReaderAt
Merkle tree library was originally using Read/Seek to access data and
tree, since the parameters are io.ReadSeeker. This could cause race
conditions if multiple threads accesses the same fd to read. Here we
change to use ReaderAt, and implement it with PRead to make it thread
safe.
PiperOrigin-RevId: 336779260
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/merkletree/merkletree.go | 130 | ||||
-rw-r--r-- | pkg/merkletree/merkletree_test.go | 52 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/verity/filesystem.go | 45 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/verity/verity.go | 76 | ||||
-rw-r--r-- | pkg/sentry/vfs/file_description.go | 24 |
5 files changed, 131 insertions, 196 deletions
diff --git a/pkg/merkletree/merkletree.go b/pkg/merkletree/merkletree.go index a6e698f57..d8227b8bd 100644 --- a/pkg/merkletree/merkletree.go +++ b/pkg/merkletree/merkletree.go @@ -138,10 +138,19 @@ func (d *VerityDescriptor) String() string { return fmt.Sprintf("Name: %s, Mode: %d, UID: %d, GID: %d, RootHash: %v", d.Name, d.Mode, d.UID, d.GID, d.RootHash) } +// verify generates a hash from d, and compares it with expected. +func (d *VerityDescriptor) verify(expected []byte) error { + h := sha256.Sum256([]byte(d.String())) + if !bytes.Equal(h[:], expected) { + return fmt.Errorf("unexpected root hash") + } + return nil +} + // GenerateParams contains the parameters used to generate a Merkle tree. type GenerateParams struct { // File is a reader of the file to be hashed. - File io.ReadSeeker + File io.ReaderAt // Size is the size of the file. Size int64 // Name is the name of the target file. @@ -153,25 +162,19 @@ type GenerateParams struct { // GID is the group ID of the target file. GID uint32 // TreeReader is a reader for the Merkle tree. - TreeReader io.ReadSeeker + TreeReader io.ReaderAt // TreeWriter is a writer for the Merkle tree. - TreeWriter io.WriteSeeker + TreeWriter io.Writer // DataAndTreeInSameFile is true if data and Merkle tree are in the same // file, or false if Merkle tree is a separate file from data. DataAndTreeInSameFile bool } -// Generate constructs a Merkle tree for the contents of data. The output is -// written to treeWriter. The treeReader should be able to read the tree after -// it has been written. That is, treeWriter and treeReader should point to the -// same underlying data but have separate cursors. +// Generate constructs a Merkle tree for the contents of params.File. The +// output is written to params.TreeWriter. // -// Generate returns a hash of descriptor. The descriptor contains the file +// Generate returns a hash of a VerityDescriptor, which contains the file // metadata and the hash from file content. -// -// Generate 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 Generate(params *GenerateParams) ([]byte, error) { layout := InitLayout(params.Size, params.DataAndTreeInSameFile) @@ -182,31 +185,11 @@ func Generate(params *GenerateParams) ([]byte, error) { bytesInLastBlock := params.Size % layout.blockSize if params.DataAndTreeInSameFile && bytesInLastBlock != 0 { zeroBuf := make([]byte, layout.blockSize-bytesInLastBlock) - if _, err := params.TreeWriter.Seek(0, io.SeekEnd); err != nil && err != io.EOF { - return nil, err - } if _, err := params.TreeWriter.Write(zeroBuf); err != nil { return nil, err } } - // Store the current offset, so we can set it back once verification - // finishes. - origOffset, err := params.File.Seek(0, io.SeekCurrent) - if err != nil { - return nil, err - } - defer params.File.Seek(origOffset, io.SeekStart) - - // Read from the beginning of both data and treeReader. - if _, err := params.File.Seek(0, io.SeekStart); err != nil && err != io.EOF { - return nil, err - } - - if _, err := params.TreeReader.Seek(0, io.SeekStart); err != nil && err != io.EOF { - return nil, err - } - var root []byte for level := 0; level < layout.numLevels(); level++ { for i := int64(0); i < numBlocks; i++ { @@ -218,11 +201,11 @@ func Generate(params *GenerateParams) ([]byte, error) { if level == 0 { // Read data block from the target file since level 0 includes hashes // of blocks in the input data. - n, err = params.File.Read(buf) + n, err = params.File.ReadAt(buf, i*layout.blockSize) } else { // Read data block from the tree file since levels higher than 0 are // hashing the lower level hashes. - n, err = params.TreeReader.Read(buf) + n, err = params.TreeReader.ReadAt(buf, layout.blockOffset(level-1, i)) } // err is populated as long as the bytes read is smaller than the buffer @@ -273,9 +256,9 @@ type VerifyParams struct { // Out will be filled with verified data. Out io.Writer // File is a handler on the file to be verified. - File io.ReadSeeker + File io.ReaderAt // tree is a handler on the Merkle tree used to verify file. - Tree io.ReadSeeker + Tree io.ReaderAt // Size is the size of the file. Size int64 // Name is the name of the target file. @@ -290,35 +273,22 @@ type VerifyParams struct { ReadOffset int64 // ReadSize is the size of the data range to be verified. ReadSize int64 - // ExpectedRoot is a trusted hash for the file. It is compared with the + // Expected is a trusted hash for the file. It is compared with the // calculated root hash to verify the content. - ExpectedRoot []byte + Expected []byte // DataAndTreeInSameFile is true if data and Merkle tree are in the same // file, or false if Merkle tree is a separate file from data. DataAndTreeInSameFile bool } -// verifyDescriptor generates a hash from descriptor, and compares it with -// expectedRoot. -func verifyDescriptor(descriptor VerityDescriptor, expectedRoot []byte) error { - h := sha256.Sum256([]byte(descriptor.String())) - if !bytes.Equal(h[:], expectedRoot) { - return fmt.Errorf("unexpected root hash") - } - return nil -} - // verifyMetadata verifies the metadata by hashing a descriptor that contains -// the metadata and compare the generated hash with expectedRoot. +// the metadata and compare the generated hash with expected. // // For verifyMetadata, params.data is not needed. It only accesses params.tree // for the raw root hash. -func verifyMetadata(params *VerifyParams, layout Layout) error { - if _, err := params.Tree.Seek(layout.blockOffset(layout.rootLevel(), 0 /* index */), io.SeekStart); err != nil { - return fmt.Errorf("failed to seek to root hash: %w", err) - } +func verifyMetadata(params *VerifyParams, layout *Layout) error { root := make([]byte, layout.digestSize) - if _, err := params.Tree.Read(root); err != nil { + if _, err := params.Tree.ReadAt(root, layout.blockOffset(layout.rootLevel(), 0 /* index */)); err != nil { return fmt.Errorf("failed to read root hash: %w", err) } descriptor := VerityDescriptor{ @@ -328,28 +298,24 @@ func verifyMetadata(params *VerifyParams, layout Layout) error { GID: params.GID, RootHash: root, } - return verifyDescriptor(descriptor, params.ExpectedRoot) + return descriptor.verify(params.Expected) } // Verify verifies the content read from data with offset. The content is // verified against tree. If content spans across multiple blocks, each block is // verified. Verification fails if the hash of the data does not match the tree -// at any level, or if the final root hash does not match expectedRoot. +// at any level, or if the final root hash does not match expected. // Once the data is verified, it will be written using params.Out. // // Verify checks for both target file content and metadata. If readSize is 0, // only metadata is checked. -// -// 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(params *VerifyParams) (int64, error) { if params.ReadSize < 0 { return 0, fmt.Errorf("unexpected read size: %d", params.ReadSize) } layout := InitLayout(int64(params.Size), params.DataAndTreeInSameFile) if params.ReadSize == 0 { - return 0, verifyMetadata(params, layout) + return 0, verifyMetadata(params, &layout) } // Calculate the index of blocks that includes the target range in input @@ -357,26 +323,13 @@ func Verify(params *VerifyParams) (int64, error) { firstDataBlock := params.ReadOffset / layout.blockSize lastDataBlock := (params.ReadOffset + params.ReadSize - 1) / layout.blockSize - // Store the current offset, so we can set it back once verification - // finishes. - origOffset, err := params.File.Seek(0, io.SeekCurrent) - if err != nil { - return 0, fmt.Errorf("find current data offset failed: %w", err) - } - defer params.File.Seek(origOffset, io.SeekStart) - - // Move to the first block that contains target data. - if _, err := params.File.Seek(firstDataBlock*layout.blockSize, io.SeekStart); err != nil { - return 0, fmt.Errorf("seek to datablock start failed: %w", err) - } - buf := make([]byte, layout.blockSize) var readErr error total := int64(0) for i := firstDataBlock; i <= lastDataBlock; i++ { // Read a block that includes all or part of target range in // input data. - bytesRead, err := params.File.Read(buf) + bytesRead, err := params.File.ReadAt(buf, i*layout.blockSize) readErr = err // If at the end of input data and all previous blocks are // verified, return the verified input data and EOF. @@ -401,7 +354,7 @@ func Verify(params *VerifyParams) (int64, error) { UID: params.UID, GID: params.GID, } - if err := verifyBlock(params.Tree, descriptor, layout, buf, i, params.ExpectedRoot); err != nil { + if err := verifyBlock(params.Tree, &descriptor, &layout, buf, i, params.Expected); err != nil { return 0, err } @@ -441,11 +394,8 @@ func Verify(params *VerifyParams) (int64, error) { // original data. The block is verified through each level of the tree. It // fails if the calculated hash from block is different from any level of // hashes stored in tree. And the final root hash is compared with -// expectedRoot. -// -// verifyBlock modifies the cursor for tree. Users needs to maintain the cursor -// if intended. -func verifyBlock(tree io.ReadSeeker, descriptor VerityDescriptor, layout Layout, dataBlock []byte, blockIndex int64, expectedRoot []byte) error { +// expected. +func verifyBlock(tree io.ReaderAt, descriptor *VerityDescriptor, layout *Layout, dataBlock []byte, blockIndex int64, expected []byte) error { if len(dataBlock) != int(layout.blockSize) { return fmt.Errorf("incorrect block size") } @@ -462,22 +412,16 @@ func verifyBlock(tree io.ReadSeeker, descriptor VerityDescriptor, layout Layout, // Read a block in previous level that contains the // hash we just generated, and generate a next level // hash from it. - if _, err := tree.Seek(layout.blockOffset(level-1, blockIndex), io.SeekStart); err != nil { - return err - } - if _, err := tree.Read(treeBlock); err != nil { + if _, err := tree.ReadAt(treeBlock, layout.blockOffset(level-1, blockIndex)); err != nil { return err } digestArray := sha256.Sum256(treeBlock) digest = digestArray[:] } - // Move to stored hash for the current block, read the digest - // and store in expectedDigest. - if _, err := tree.Seek(layout.digestOffset(level, blockIndex), io.SeekStart); err != nil { - return err - } - if _, err := tree.Read(expectedDigest); err != nil { + // Read the digest for the current block and store in + // expectedDigest. + if _, err := tree.ReadAt(expectedDigest, layout.digestOffset(level, blockIndex)); err != nil { return err } @@ -488,7 +432,7 @@ func verifyBlock(tree io.ReadSeeker, descriptor VerityDescriptor, layout Layout, } // Verification for the tree succeeded. Now hash the descriptor with - // the root hash and compare it with expectedRoot. + // the root hash and compare it with expected. descriptor.RootHash = digest - return verifyDescriptor(descriptor, expectedRoot) + return descriptor.verify(expected) } diff --git a/pkg/merkletree/merkletree_test.go b/pkg/merkletree/merkletree_test.go index bb11ec844..e1350ebda 100644 --- a/pkg/merkletree/merkletree_test.go +++ b/pkg/merkletree/merkletree_test.go @@ -106,58 +106,36 @@ func (brw *bytesReadWriter) Write(p []byte) (int, error) { return len(p), nil } -func (brw *bytesReadWriter) Read(p []byte) (int, error) { - if brw.readPos >= len(brw.bytes) { - return 0, io.EOF - } - bytesRead := copy(p, brw.bytes[brw.readPos:]) - brw.readPos += bytesRead - if bytesRead < len(p) { +func (brw *bytesReadWriter) ReadAt(p []byte, off int64) (int, error) { + bytesRead := copy(p, brw.bytes[off:]) + if bytesRead == 0 { return bytesRead, io.EOF } return bytesRead, nil } -func (brw *bytesReadWriter) Seek(offset int64, whence int) (int64, error) { - off := offset - if whence == io.SeekCurrent { - off += int64(brw.readPos) - } - if whence == io.SeekEnd { - off += int64(len(brw.bytes)) - } - if off < 0 { - panic("seek with negative offset") - } - if off >= int64(len(brw.bytes)) { - return 0, io.EOF - } - brw.readPos = int(off) - return off, nil -} - func TestGenerate(t *testing.T) { // The input data has size dataSize. It starts with the data in startWith, // and all other bytes are zeroes. testCases := []struct { data []byte - expectedRoot []byte + expectedHash []byte }{ { data: bytes.Repeat([]byte{0}, usermem.PageSize), - expectedRoot: []byte{64, 253, 58, 72, 192, 131, 82, 184, 193, 33, 108, 142, 43, 46, 179, 134, 244, 21, 29, 190, 14, 39, 66, 129, 6, 46, 200, 211, 30, 247, 191, 252}, + expectedHash: []byte{64, 253, 58, 72, 192, 131, 82, 184, 193, 33, 108, 142, 43, 46, 179, 134, 244, 21, 29, 190, 14, 39, 66, 129, 6, 46, 200, 211, 30, 247, 191, 252}, }, { data: bytes.Repeat([]byte{0}, 128*usermem.PageSize+1), - expectedRoot: []byte{182, 223, 218, 62, 65, 185, 160, 219, 93, 119, 186, 88, 205, 32, 122, 231, 173, 72, 78, 76, 65, 57, 177, 146, 159, 39, 44, 123, 230, 156, 97, 26}, + expectedHash: []byte{182, 223, 218, 62, 65, 185, 160, 219, 93, 119, 186, 88, 205, 32, 122, 231, 173, 72, 78, 76, 65, 57, 177, 146, 159, 39, 44, 123, 230, 156, 97, 26}, }, { data: []byte{'a'}, - expectedRoot: []byte{28, 201, 8, 36, 150, 178, 111, 5, 193, 212, 129, 205, 206, 124, 211, 90, 224, 142, 81, 183, 72, 165, 243, 240, 242, 241, 76, 127, 101, 61, 63, 11}, + expectedHash: []byte{28, 201, 8, 36, 150, 178, 111, 5, 193, 212, 129, 205, 206, 124, 211, 90, 224, 142, 81, 183, 72, 165, 243, 240, 242, 241, 76, 127, 101, 61, 63, 11}, }, { data: bytes.Repeat([]byte{'a'}, usermem.PageSize), - expectedRoot: []byte{106, 58, 160, 152, 41, 68, 38, 108, 245, 74, 177, 84, 64, 193, 19, 176, 249, 86, 27, 193, 85, 164, 99, 240, 79, 104, 148, 222, 76, 46, 191, 79}, + expectedHash: []byte{106, 58, 160, 152, 41, 68, 38, 108, 245, 74, 177, 84, 64, 193, 19, 176, 249, 86, 27, 193, 85, 164, 99, 240, 79, 104, 148, 222, 76, 46, 191, 79}, }, } @@ -183,13 +161,13 @@ func TestGenerate(t *testing.T) { bytes: tc.data, } } - root, err := Generate(¶ms) + hash, err := Generate(¶ms) if err != nil { t.Fatalf("Got err: %v, want nil", err) } - if !bytes.Equal(root, tc.expectedRoot) { - t.Errorf("Got root: %v, want %v", root, tc.expectedRoot) + if !bytes.Equal(hash, tc.expectedHash) { + t.Errorf("Got hash: %v, want %v", hash, tc.expectedHash) } } }) @@ -390,7 +368,7 @@ func TestVerify(t *testing.T) { bytes: data, } } - root, err := Generate(&genParams) + hash, err := Generate(&genParams) if err != nil { t.Fatalf("Generate failed: %v", err) } @@ -409,7 +387,7 @@ func TestVerify(t *testing.T) { GID: defaultGID, ReadOffset: tc.verifyStart, ReadSize: tc.verifySize, - ExpectedRoot: root, + Expected: hash, DataAndTreeInSameFile: dataAndTreeInSameFile, } if tc.modifyName { @@ -478,7 +456,7 @@ func TestVerifyRandom(t *testing.T) { bytes: data, } } - root, err := Generate(&genParams) + hash, err := Generate(&genParams) if err != nil { t.Fatalf("Generate failed: %v", err) } @@ -499,7 +477,7 @@ func TestVerifyRandom(t *testing.T) { GID: defaultGID, ReadOffset: start, ReadSize: size, - ExpectedRoot: root, + Expected: hash, DataAndTreeInSameFile: dataAndTreeInSameFile, } diff --git a/pkg/sentry/fsimpl/verity/filesystem.go b/pkg/sentry/fsimpl/verity/filesystem.go index 7779271a9..34e2c9d7c 100644 --- a/pkg/sentry/fsimpl/verity/filesystem.go +++ b/pkg/sentry/fsimpl/verity/filesystem.go @@ -156,11 +156,10 @@ afterSymlink: return child, nil } -// verifyChild verifies the root hash of child against the already verified -// root hash of the parent to ensure the child is expected. verifyChild -// triggers a sentry panic if unexpected modifications to the file system are -// detected. In noCrashOnVerificationFailure mode it returns a syserror -// instead. +// verifyChild verifies the hash of child against the already verified hash of +// the parent to ensure the child is expected. verifyChild triggers a sentry +// panic if unexpected modifications to the file system are detected. In +// noCrashOnVerificationFailure mode it returns a syserror instead. // Preconditions: fs.renameMu must be locked. d.dirMu must be locked. // TODO(b/166474175): Investigate all possible errors returned in this // function, and make sure we differentiate all errors that indicate unexpected @@ -179,8 +178,8 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de defer verityMu.RUnlock() // Read the offset of the child from the extended attributes of the // corresponding Merkle tree file. - // This is the offset of the root hash for child in its parent's Merkle - // tree file. + // This is the offset of the hash for child in its parent's Merkle tree + // file. off, err := vfsObj.GetXattrAt(ctx, fs.creds, &vfs.PathOperation{ Root: child.lowerMerkleVD, Start: child.lowerMerkleVD, @@ -205,7 +204,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de 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. + // Open parent Merkle tree file to read and verify child's hash. parentMerkleFD, err := vfsObj.OpenAt(ctx, fs.creds, &vfs.PathOperation{ Root: parent.lowerMerkleVD, Start: parent.lowerMerkleVD, @@ -224,7 +223,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de // dataSize is the size of raw data for the Merkle tree. For a file, // dataSize is the size of the whole file. For a directory, dataSize is - // the size of all its children's root hashes. + // the size of all its children's hashes. dataSize, err := parentMerkleFD.GetXattr(ctx, &vfs.GetXattrOptions{ Name: merkleSizeXattr, Size: sizeOfStringInt32, @@ -264,7 +263,7 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de } // Since we are verifying against a directory Merkle tree, buf should - // contain the root hash of the children in the parent Merkle tree when + // contain the hash of the children in the parent Merkle tree when // Verify returns with success. var buf bytes.Buffer if _, err := merkletree.Verify(&merkletree.VerifyParams{ @@ -278,21 +277,21 @@ func (fs *filesystem) verifyChild(ctx context.Context, parent *dentry, child *de GID: parentStat.GID, ReadOffset: int64(offset), ReadSize: int64(merkletree.DigestSize()), - ExpectedRoot: parent.rootHash, + Expected: parent.hash, DataAndTreeInSameFile: true, }); 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. - if len(child.rootHash) == 0 { - child.rootHash = buf.Bytes() + // Cache child hash when it's verified the first time. + if len(child.hash) == 0 { + child.hash = buf.Bytes() } return child, nil } -// verifyStat verifies the stat against the verified root hash. The mode/uid/gid -// of the file is cached after verified. +// verifyStat verifies the stat against the verified hash. The mode/uid/gid of +// the file is cached after verified. func (fs *filesystem) verifyStat(ctx context.Context, d *dentry, stat linux.Statx) error { vfsObj := fs.vfsfs.VirtualFilesystem() @@ -353,7 +352,7 @@ func (fs *filesystem) verifyStat(ctx context.Context, d *dentry, stat linux.Stat ReadOffset: 0, // Set read size to 0 so only the metadata is verified. ReadSize: 0, - ExpectedRoot: d.rootHash, + Expected: d.hash, DataAndTreeInSameFile: false, } if atomic.LoadUint32(&d.mode)&linux.S_IFMT == linux.S_IFDIR { @@ -375,8 +374,8 @@ func (fs *filesystem) getChildLocked(ctx context.Context, parent *dentry, name s // If enabling verification on files/directories is not allowed // during runtime, all cached children are already verified. If // runtime enable is allowed and the parent directory is - // enabled, we should verify the child root hash here because - // it may be cached before enabled. + // enabled, we should verify the child hash here because it may + // be cached before enabled. if fs.allowRuntimeEnable { if isEnabled(parent) { if _, err := fs.verifyChild(ctx, parent, child); err != nil { @@ -481,9 +480,9 @@ func (fs *filesystem) lookupAndVerifyLocked(ctx context.Context, parent *dentry, // file open and ready to use. // This may cause empty and unused Merkle tree files in // allowRuntimeEnable mode, if they are never enabled. This - // does not affect verification, as we rely on cached root hash - // to decide whether to perform verification, not the existence - // of the Merkle tree file. Also, those Merkle tree files are + // does not affect verification, as we rely on cached hash to + // decide whether to perform verification, not the existence of + // the Merkle tree file. Also, those Merkle tree files are // always hidden and cannot be accessed by verity fs users. if fs.allowRuntimeEnable { childMerkleFD, err := vfsObj.OpenAt(ctx, fs.creds, &vfs.PathOperation{ @@ -551,7 +550,7 @@ func (fs *filesystem) lookupAndVerifyLocked(ctx context.Context, parent *dentry, child.uid = stat.UID child.gid = stat.GID - // Verify child root hash. This should always be performed unless in + // Verify child hash. This should always be performed unless in // allowRuntimeEnable mode and the parent directory hasn't been enabled // yet. if isEnabled(parent) { diff --git a/pkg/sentry/fsimpl/verity/verity.go b/pkg/sentry/fsimpl/verity/verity.go index 3eb972237..4a6708633 100644 --- a/pkg/sentry/fsimpl/verity/verity.go +++ b/pkg/sentry/fsimpl/verity/verity.go @@ -49,12 +49,12 @@ const Name = "verity" const merklePrefix = ".merkle.verity." // merkleoffsetInParentXattr is the extended attribute name specifying the -// offset of child root hash in its parent's Merkle tree. +// offset of child hash in its parent's Merkle tree. const merkleOffsetInParentXattr = "user.merkle.offset" // merkleSizeXattr is the extended attribute name specifying the size of data // hashed by the corresponding Merkle tree. For a file, it's the size of the -// whole file. For a directory, it's the size of all its children's root hashes. +// whole file. For a directory, it's the size of all its children's hashes. const merkleSizeXattr = "user.merkle.size" // sizeOfStringInt32 is the size for a 32 bit integer stored as string in @@ -147,7 +147,7 @@ func (FilesystemType) Name() string { // mode, it returns true if the target has been enabled with // ioctl(FS_IOC_ENABLE_VERITY). func isEnabled(d *dentry) bool { - return !d.fs.allowRuntimeEnable || len(d.rootHash) != 0 + return !d.fs.allowRuntimeEnable || len(d.hash) != 0 } // alertIntegrityViolation alerts a violation of integrity, which usually means @@ -256,7 +256,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt d.mode = uint32(stat.Mode) d.uid = stat.UID d.gid = stat.GID - d.rootHash = make([]byte, len(iopts.RootHash)) + d.hash = make([]byte, len(iopts.RootHash)) if !fs.allowRuntimeEnable { if err := fs.verifyStat(ctx, d, stat); err != nil { @@ -264,7 +264,7 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt } } - copy(d.rootHash, iopts.RootHash) + copy(d.hash, iopts.RootHash) d.vfsd.Init(d) fs.rootDentry = d @@ -316,8 +316,8 @@ type dentry struct { // in the underlying file system. lowerMerkleVD vfs.VirtualDentry - // rootHash is the rootHash for the current file or directory. - rootHash []byte + // hash is the calculated hash for the current file or directory. + hash []byte } // newDentry creates a new dentry representing the given verity file. The @@ -516,11 +516,11 @@ func (fd *fileDescription) SetStat(ctx context.Context, opts vfs.SetStatOptions) } // generateMerkle generates a Merkle tree file for fd. If fd points to a file -// /foo/bar, a Merkle tree file /foo/.merkle.verity.bar is generated. The root -// 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 root hahes of its children, written -// to the Merkle tree file. +// /foo/bar, a Merkle tree file /foo/.merkle.verity.bar is generated. The 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 hahes of its children, written to the Merkle tree +// file. func (fd *fileDescription) generateMerkle(ctx context.Context) ([]byte, uint64, error) { fdReader := vfs.FileReadWriteSeeker{ FD: fd.lowerFD, @@ -557,9 +557,9 @@ func (fd *fileDescription) generateMerkle(ctx context.Context) ([]byte, uint64, params.GID = stat.GID params.DataAndTreeInSameFile = false case linux.S_IFDIR: - // For a directory, generate a Merkle tree based on the root - // hashes of its children that has already been written to the - // Merkle tree file. + // For a directory, generate a Merkle tree based on the hashes + // of its children that has already been written to the Merkle + // tree file. merkleStat, err := fd.merkleReader.Stat(ctx, vfs.StatOptions{}) if err != nil { return nil, 0, err @@ -583,12 +583,12 @@ func (fd *fileDescription) generateMerkle(ctx context.Context) ([]byte, uint64, // enable other types of file. return nil, 0, syserror.EINVAL } - rootHash, err := merkletree.Generate(params) - return rootHash, uint64(params.Size), err + hash, err := merkletree.Generate(params) + return hash, uint64(params.Size), err } // enableVerity enables verity features on fd by generating a Merkle tree file -// and stores its root hash in its parent directory's Merkle tree. +// and stores its hash in its parent directory's Merkle tree. func (fd *fileDescription) enableVerity(ctx context.Context, uio usermem.IO) (uintptr, error) { if !fd.d.fs.allowRuntimeEnable { return 0, syserror.EPERM @@ -607,7 +607,7 @@ func (fd *fileDescription) enableVerity(ctx context.Context, uio usermem.IO) (ui return 0, alertIntegrityViolation(syserror.EIO, "Unexpected verity fd: missing expected underlying fds") } - rootHash, dataSize, err := fd.generateMerkle(ctx) + hash, dataSize, err := fd.generateMerkle(ctx) if err != nil { return 0, err } @@ -618,15 +618,15 @@ func (fd *fileDescription) enableVerity(ctx context.Context, uio usermem.IO) (ui return 0, err } - // Write the root hash of fd to the parent directory's Merkle - // tree file, as it should be part of the parent Merkle tree - // data. parentMerkleWriter is open with O_APPEND, so it - // should write directly to the end of the file. - if _, err = fd.parentMerkleWriter.Write(ctx, usermem.BytesIOSequence(rootHash), vfs.WriteOptions{}); err != nil { + // Write the hash of fd to the parent directory's Merkle tree + // file, as it should be part of the parent Merkle tree data. + // parentMerkleWriter is open with O_APPEND, so it should write + // directly to the end of the file. + if _, err = fd.parentMerkleWriter.Write(ctx, usermem.BytesIOSequence(hash), vfs.WriteOptions{}); err != nil { return 0, err } - // Record the offset of the root hash of fd in parent directory's + // Record the offset of the hash of fd in parent directory's // Merkle tree file. if err := fd.merkleWriter.SetXattr(ctx, &vfs.SetXattrOptions{ Name: merkleOffsetInParentXattr, @@ -643,37 +643,37 @@ func (fd *fileDescription) enableVerity(ctx context.Context, uio usermem.IO) (ui }); err != nil { return 0, err } - fd.d.rootHash = append(fd.d.rootHash, rootHash...) + fd.d.hash = append(fd.d.hash, hash...) return 0, nil } -// measureVerity returns the root hash of fd, saved in args[2]. +// measureVerity returns the hash of fd, saved in verityDigest. func (fd *fileDescription) measureVerity(ctx context.Context, uio usermem.IO, verityDigest usermem.Addr) (uintptr, error) { t := kernel.TaskFromContext(ctx) var metadata linux.DigestMetadata - // If allowRuntimeEnable is true, an empty fd.d.rootHash indicates that + // If allowRuntimeEnable is true, an empty fd.d.hash indicates that // verity is not enabled for the file. If allowRuntimeEnable is false, // this is an integrity violation because all files should have verity - // enabled, in which case fd.d.rootHash should be set. - if len(fd.d.rootHash) == 0 { + // enabled, in which case fd.d.hash should be set. + if len(fd.d.hash) == 0 { if fd.d.fs.allowRuntimeEnable { return 0, syserror.ENODATA } - return 0, alertIntegrityViolation(syserror.ENODATA, "Ioctl measureVerity: no root hash found") + return 0, alertIntegrityViolation(syserror.ENODATA, "Ioctl measureVerity: no hash found") } // The first part of VerityDigest is the metadata. if _, err := metadata.CopyIn(t, verityDigest); err != nil { return 0, err } - if metadata.DigestSize < uint16(len(fd.d.rootHash)) { + if metadata.DigestSize < uint16(len(fd.d.hash)) { return 0, syserror.EOVERFLOW } // Populate the output digest size, since DigestSize is both input and // output. - metadata.DigestSize = uint16(len(fd.d.rootHash)) + metadata.DigestSize = uint16(len(fd.d.hash)) // First copy the metadata. if _, err := metadata.CopyOut(t, verityDigest); err != nil { @@ -681,16 +681,16 @@ func (fd *fileDescription) measureVerity(ctx context.Context, uio usermem.IO, ve } // Now copy the root hash bytes to the memory after metadata. - _, err := t.CopyOutBytes(usermem.Addr(uintptr(verityDigest)+linux.SizeOfDigestMetadata), fd.d.rootHash) + _, err := t.CopyOutBytes(usermem.Addr(uintptr(verityDigest)+linux.SizeOfDigestMetadata), fd.d.hash) return 0, err } func (fd *fileDescription) verityFlags(ctx context.Context, uio usermem.IO, flags usermem.Addr) (uintptr, error) { f := int32(0) - // All enabled files should store a root hash. This flag is not settable - // via FS_IOC_SETFLAGS. - if len(fd.d.rootHash) != 0 { + // All enabled files should store a hash. This flag is not settable via + // FS_IOC_SETFLAGS. + if len(fd.d.hash) != 0 { f |= linux.FS_VERITY_FL } @@ -767,7 +767,7 @@ func (fd *fileDescription) PRead(ctx context.Context, dst usermem.IOSequence, of GID: fd.d.gid, ReadOffset: offset, ReadSize: dst.NumBytes(), - ExpectedRoot: fd.d.rootHash, + Expected: fd.d.hash, DataAndTreeInSameFile: false, }) if err != nil { diff --git a/pkg/sentry/vfs/file_description.go b/pkg/sentry/vfs/file_description.go index 1eba0270f..183957ad8 100644 --- a/pkg/sentry/vfs/file_description.go +++ b/pkg/sentry/vfs/file_description.go @@ -827,7 +827,7 @@ func (fd *FileDescription) SetAsyncHandler(newHandler func() FileAsync) FileAsyn } // FileReadWriteSeeker is a helper struct to pass a FileDescription as -// io.Reader/io.Writer/io.ReadSeeker/etc. +// io.Reader/io.Writer/io.ReadSeeker/io.ReaderAt/io.WriterAt/etc. type FileReadWriteSeeker struct { FD *FileDescription Ctx context.Context @@ -835,11 +835,18 @@ type FileReadWriteSeeker struct { WOpts WriteOptions } +// ReadAt implements io.ReaderAt.ReadAt. +func (f *FileReadWriteSeeker) ReadAt(p []byte, off int64) (int, error) { + dst := usermem.BytesIOSequence(p) + n, err := f.FD.PRead(f.Ctx, dst, off, f.ROpts) + return int(n), err +} + // Read implements io.ReadWriteSeeker.Read. func (f *FileReadWriteSeeker) Read(p []byte) (int, error) { dst := usermem.BytesIOSequence(p) - ret, err := f.FD.Read(f.Ctx, dst, f.ROpts) - return int(ret), err + n, err := f.FD.Read(f.Ctx, dst, f.ROpts) + return int(n), err } // Seek implements io.ReadWriteSeeker.Seek. @@ -847,9 +854,16 @@ func (f *FileReadWriteSeeker) Seek(offset int64, whence int) (int64, error) { return f.FD.Seek(f.Ctx, offset, int32(whence)) } +// WriteAt implements io.WriterAt.WriteAt. +func (f *FileReadWriteSeeker) WriteAt(p []byte, off int64) (int, error) { + dst := usermem.BytesIOSequence(p) + n, err := f.FD.PWrite(f.Ctx, dst, off, f.WOpts) + return int(n), err +} + // Write implements io.ReadWriteSeeker.Write. func (f *FileReadWriteSeeker) Write(p []byte) (int, error) { buf := usermem.BytesIOSequence(p) - ret, err := f.FD.Write(f.Ctx, buf, f.WOpts) - return int(ret), err + n, err := f.FD.Write(f.Ctx, buf, f.WOpts) + return int(n), err } |