diff options
-rw-r--r-- | pkg/merkletree/merkletree.go | 81 | ||||
-rw-r--r-- | pkg/merkletree/merkletree_test.go | 121 | ||||
-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 |
6 files changed, 605 insertions, 92 deletions
diff --git a/pkg/merkletree/merkletree.go b/pkg/merkletree/merkletree.go index aea7dde38..d7209ace3 100644 --- a/pkg/merkletree/merkletree.go +++ b/pkg/merkletree/merkletree.go @@ -147,20 +147,21 @@ func (layout Layout) blockOffset(level int, index int64) int64 { // root hash, which contains the root hash of the raw content and the file's // meatadata. type VerityDescriptor struct { - Name string - FileSize int64 - Mode uint32 - UID uint32 - GID uint32 - Children map[string]struct{} - RootHash []byte + Name string + FileSize int64 + Mode uint32 + UID uint32 + GID uint32 + Children map[string]struct{} + SymlinkTarget string + RootHash []byte } func (d *VerityDescriptor) String() string { b := new(bytes.Buffer) e := gob.NewEncoder(b) e.Encode(d.Children) - return fmt.Sprintf("Name: %s, Size: %d, Mode: %d, UID: %d, GID: %d, Children: %v, RootHash: %v", d.Name, d.FileSize, d.Mode, d.UID, d.GID, b.Bytes(), d.RootHash) + return fmt.Sprintf("Name: %s, Size: %d, Mode: %d, UID: %d, GID: %d, Children: %v, Symlink: %s, RootHash: %v", d.Name, d.FileSize, d.Mode, d.UID, d.GID, b.Bytes(), d.SymlinkTarget, d.RootHash) } // verify generates a hash from d, and compares it with expected. @@ -193,7 +194,8 @@ func hashData(data []byte, hashAlgorithms int) ([]byte, error) { return digest, nil } -// GenerateParams contains the parameters used to generate a Merkle tree. +// GenerateParams contains the parameters used to generate a Merkle tree for a +// given file. type GenerateParams struct { // File is a reader of the file to be hashed. File io.ReaderAt @@ -210,6 +212,8 @@ type GenerateParams struct { // Children is a map of children names for a directory. It should be // empty for a regular file. Children map[string]struct{} + // SymlinkTarget is the target path of a symlink file, or "" if the file is not a symlink. + SymlinkTarget string // HashAlgorithms is the algorithms used to hash data. HashAlgorithms int // TreeReader is a reader for the Merkle tree. @@ -227,6 +231,20 @@ type GenerateParams struct { // Generate returns a hash of a VerityDescriptor, which contains the file // metadata and the hash from file content. func Generate(params *GenerateParams) ([]byte, error) { + descriptor := VerityDescriptor{ + FileSize: params.Size, + Name: params.Name, + Mode: params.Mode, + UID: params.UID, + GID: params.GID, + SymlinkTarget: params.SymlinkTarget, + } + + // If file is a symlink do not generate root hash for file content. + if params.SymlinkTarget != "" { + return hashData([]byte(descriptor.String()), params.HashAlgorithms) + } + layout, err := InitLayout(params.Size, params.HashAlgorithms, params.DataAndTreeInSameFile) if err != nil { return nil, err @@ -296,15 +314,7 @@ func Generate(params *GenerateParams) ([]byte, error) { } numBlocks = (numBlocks + layout.hashesPerBlock() - 1) / layout.hashesPerBlock() } - descriptor := VerityDescriptor{ - Name: params.Name, - FileSize: params.Size, - Mode: params.Mode, - UID: params.UID, - GID: params.GID, - Children: params.Children, - RootHash: root, - } + descriptor.RootHash = root return hashData([]byte(descriptor.String()), params.HashAlgorithms) } @@ -330,6 +340,8 @@ type VerifyParams struct { // Children is a map of children names for a directory. It should be // empty for a regular file. Children map[string]struct{} + // SymlinkTarget is the target path of a symlink file, or "" if the file is not a symlink. + SymlinkTarget string // HashAlgorithms is the algorithms used to hash data. HashAlgorithms int // ReadOffset is the offset of the data range to be verified. @@ -351,21 +363,23 @@ type VerifyParams struct { // for the raw root hash. func verifyMetadata(params *VerifyParams, layout *Layout) error { var root []byte - // Only read the root hash if we expect that the Merkle tree file is non-empty. - if params.Size != 0 { + // Only read the root hash if we expect that the file is not a symlink and its + // Merkle tree file is non-empty. + if params.Size != 0 && params.SymlinkTarget == "" { root = make([]byte, layout.digestSize) 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{ - Name: params.Name, - FileSize: params.Size, - Mode: params.Mode, - UID: params.UID, - GID: params.GID, - Children: params.Children, - RootHash: root, + Name: params.Name, + FileSize: params.Size, + Mode: params.Mode, + UID: params.UID, + GID: params.GID, + Children: params.Children, + SymlinkTarget: params.SymlinkTarget, + RootHash: root, } return descriptor.verify(params.Expected, params.HashAlgorithms) } @@ -421,12 +435,13 @@ func Verify(params *VerifyParams) (int64, error) { } } descriptor := VerityDescriptor{ - Name: params.Name, - FileSize: params.Size, - Mode: params.Mode, - UID: params.UID, - GID: params.GID, - Children: params.Children, + Name: params.Name, + FileSize: params.Size, + Mode: params.Mode, + UID: params.UID, + GID: params.GID, + SymlinkTarget: params.SymlinkTarget, + Children: params.Children, } if err := verifyBlock(params.Tree, &descriptor, &layout, buf, i, params.HashAlgorithms, params.Expected); err != nil { return 0, err diff --git a/pkg/merkletree/merkletree_test.go b/pkg/merkletree/merkletree_test.go index 66ddf09e6..e3a88b3a3 100644 --- a/pkg/merkletree/merkletree_test.go +++ b/pkg/merkletree/merkletree_test.go @@ -159,10 +159,11 @@ func TestLayout(t *testing.T) { } const ( - defaultName = "merkle_test" - defaultMode = 0644 - defaultUID = 0 - defaultGID = 0 + defaultName = "merkle_test" + defaultMode = 0644 + defaultUID = 0 + defaultGID = 0 + defaultSymlinkPath = "merkle_test_link" ) // bytesReadWriter is used to read from/write to/seek in a byte array. Unlike @@ -203,112 +204,112 @@ func TestGenerate(t *testing.T) { data: bytes.Repeat([]byte{0}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: false, - expectedHash: []byte{42, 197, 191, 52, 206, 122, 93, 34, 198, 125, 100, 154, 171, 177, 94, 14, 49, 40, 76, 157, 122, 58, 78, 6, 163, 248, 30, 238, 16, 190, 173, 175}, + expectedHash: []byte{9, 115, 238, 230, 38, 140, 195, 70, 207, 144, 202, 118, 23, 113, 32, 129, 226, 239, 177, 69, 161, 26, 14, 113, 16, 37, 30, 96, 19, 148, 132, 27}, }, { name: "OnePageZeroesSHA256SameFile", data: bytes.Repeat([]byte{0}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: true, - expectedHash: []byte{42, 197, 191, 52, 206, 122, 93, 34, 198, 125, 100, 154, 171, 177, 94, 14, 49, 40, 76, 157, 122, 58, 78, 6, 163, 248, 30, 238, 16, 190, 173, 175}, + expectedHash: []byte{9, 115, 238, 230, 38, 140, 195, 70, 207, 144, 202, 118, 23, 113, 32, 129, 226, 239, 177, 69, 161, 26, 14, 113, 16, 37, 30, 96, 19, 148, 132, 27}, }, { name: "OnePageZeroesSHA512SeparateFile", data: bytes.Repeat([]byte{0}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: false, - expectedHash: []byte{87, 131, 150, 74, 0, 218, 117, 114, 34, 23, 212, 16, 122, 97, 124, 172, 41, 46, 107, 150, 33, 46, 56, 39, 5, 246, 215, 187, 140, 83, 35, 63, 111, 74, 155, 241, 161, 214, 92, 141, 232, 125, 99, 71, 168, 102, 82, 20, 229, 249, 248, 28, 29, 238, 199, 223, 173, 180, 179, 46, 241, 240, 237, 74}, + expectedHash: []byte{127, 8, 95, 11, 83, 101, 51, 39, 170, 235, 39, 43, 135, 243, 145, 118, 148, 58, 27, 155, 182, 205, 44, 47, 5, 223, 215, 17, 35, 16, 43, 104, 43, 11, 8, 88, 171, 7, 249, 243, 14, 62, 126, 218, 23, 159, 237, 237, 42, 226, 39, 25, 87, 48, 253, 191, 116, 213, 37, 3, 187, 152, 154, 14}, }, { name: "OnePageZeroesSHA512SameFile", data: bytes.Repeat([]byte{0}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: true, - expectedHash: []byte{87, 131, 150, 74, 0, 218, 117, 114, 34, 23, 212, 16, 122, 97, 124, 172, 41, 46, 107, 150, 33, 46, 56, 39, 5, 246, 215, 187, 140, 83, 35, 63, 111, 74, 155, 241, 161, 214, 92, 141, 232, 125, 99, 71, 168, 102, 82, 20, 229, 249, 248, 28, 29, 238, 199, 223, 173, 180, 179, 46, 241, 240, 237, 74}, + expectedHash: []byte{127, 8, 95, 11, 83, 101, 51, 39, 170, 235, 39, 43, 135, 243, 145, 118, 148, 58, 27, 155, 182, 205, 44, 47, 5, 223, 215, 17, 35, 16, 43, 104, 43, 11, 8, 88, 171, 7, 249, 243, 14, 62, 126, 218, 23, 159, 237, 237, 42, 226, 39, 25, 87, 48, 253, 191, 116, 213, 37, 3, 187, 152, 154, 14}, }, { name: "MultiplePageZeroesSHA256SeparateFile", data: bytes.Repeat([]byte{0}, 128*usermem.PageSize+1), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: false, - expectedHash: []byte{115, 151, 35, 147, 223, 91, 17, 6, 162, 145, 237, 81, 88, 53, 120, 49, 128, 70, 188, 28, 254, 241, 19, 233, 30, 243, 71, 225, 57, 58, 61, 38}, + expectedHash: []byte{247, 158, 42, 215, 180, 106, 0, 28, 77, 64, 132, 162, 74, 65, 250, 161, 243, 66, 129, 44, 197, 8, 145, 14, 94, 206, 156, 184, 145, 145, 20, 185}, }, { name: "MultiplePageZeroesSHA256SameFile", data: bytes.Repeat([]byte{0}, 128*usermem.PageSize+1), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: true, - expectedHash: []byte{115, 151, 35, 147, 223, 91, 17, 6, 162, 145, 237, 81, 88, 53, 120, 49, 128, 70, 188, 28, 254, 241, 19, 233, 30, 243, 71, 225, 57, 58, 61, 38}, + expectedHash: []byte{247, 158, 42, 215, 180, 106, 0, 28, 77, 64, 132, 162, 74, 65, 250, 161, 243, 66, 129, 44, 197, 8, 145, 14, 94, 206, 156, 184, 145, 145, 20, 185}, }, { name: "MultiplePageZeroesSHA512SeparateFile", data: bytes.Repeat([]byte{0}, 128*usermem.PageSize+1), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: false, - expectedHash: []byte{41, 94, 205, 97, 254, 226, 171, 69, 76, 102, 197, 47, 113, 53, 24, 244, 103, 131, 83, 73, 87, 212, 247, 140, 32, 144, 211, 158, 25, 131, 194, 57, 21, 224, 128, 119, 69, 100, 45, 50, 157, 54, 46, 214, 152, 179, 59, 78, 28, 48, 146, 160, 204, 48, 27, 90, 152, 193, 167, 45, 150, 67, 66, 217}, + expectedHash: []byte{100, 121, 14, 30, 104, 200, 142, 182, 190, 78, 23, 68, 157, 174, 23, 75, 174, 250, 250, 25, 66, 45, 235, 103, 129, 49, 78, 127, 173, 154, 121, 35, 37, 115, 60, 217, 26, 205, 253, 253, 236, 145, 107, 109, 232, 19, 72, 92, 4, 191, 181, 205, 191, 57, 234, 177, 144, 235, 143, 30, 15, 197, 109, 81}, }, { name: "MultiplePageZeroesSHA512SameFile", data: bytes.Repeat([]byte{0}, 128*usermem.PageSize+1), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: true, - expectedHash: []byte{41, 94, 205, 97, 254, 226, 171, 69, 76, 102, 197, 47, 113, 53, 24, 244, 103, 131, 83, 73, 87, 212, 247, 140, 32, 144, 211, 158, 25, 131, 194, 57, 21, 224, 128, 119, 69, 100, 45, 50, 157, 54, 46, 214, 152, 179, 59, 78, 28, 48, 146, 160, 204, 48, 27, 90, 152, 193, 167, 45, 150, 67, 66, 217}, + expectedHash: []byte{100, 121, 14, 30, 104, 200, 142, 182, 190, 78, 23, 68, 157, 174, 23, 75, 174, 250, 250, 25, 66, 45, 235, 103, 129, 49, 78, 127, 173, 154, 121, 35, 37, 115, 60, 217, 26, 205, 253, 253, 236, 145, 107, 109, 232, 19, 72, 92, 4, 191, 181, 205, 191, 57, 234, 177, 144, 235, 143, 30, 15, 197, 109, 81}, }, { name: "SingleASHA256SeparateFile", data: []byte{'a'}, hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: false, - expectedHash: []byte{52, 159, 140, 206, 140, 138, 231, 140, 94, 14, 252, 66, 175, 128, 191, 14, 52, 215, 190, 184, 165, 50, 182, 224, 42, 156, 145, 0, 1, 15, 187, 85}, + expectedHash: []byte{90, 124, 194, 100, 206, 242, 75, 152, 47, 249, 16, 27, 136, 161, 223, 228, 121, 241, 126, 158, 126, 122, 100, 120, 117, 15, 81, 78, 201, 133, 119, 111}, }, { name: "SingleASHA256SameFile", data: []byte{'a'}, hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: true, - expectedHash: []byte{52, 159, 140, 206, 140, 138, 231, 140, 94, 14, 252, 66, 175, 128, 191, 14, 52, 215, 190, 184, 165, 50, 182, 224, 42, 156, 145, 0, 1, 15, 187, 85}, + expectedHash: []byte{90, 124, 194, 100, 206, 242, 75, 152, 47, 249, 16, 27, 136, 161, 223, 228, 121, 241, 126, 158, 126, 122, 100, 120, 117, 15, 81, 78, 201, 133, 119, 111}, }, { name: "SingleASHA512SeparateFile", data: []byte{'a'}, hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: false, - expectedHash: []byte{232, 90, 223, 95, 60, 151, 149, 172, 174, 58, 206, 97, 189, 103, 6, 202, 67, 248, 1, 189, 243, 51, 250, 42, 5, 89, 195, 9, 50, 74, 39, 169, 114, 228, 109, 225, 128, 210, 63, 94, 18, 133, 58, 48, 225, 100, 176, 55, 87, 60, 235, 224, 143, 41, 15, 253, 94, 28, 251, 233, 99, 207, 152, 108}, + expectedHash: []byte{24, 10, 13, 25, 113, 62, 169, 99, 151, 70, 166, 113, 81, 81, 163, 85, 5, 25, 29, 15, 46, 37, 104, 120, 142, 218, 52, 178, 187, 83, 30, 166, 101, 87, 70, 196, 188, 61, 123, 20, 13, 254, 126, 52, 212, 111, 75, 203, 33, 233, 233, 47, 181, 161, 43, 193, 131, 41, 99, 33, 164, 73, 89, 152}, }, { name: "SingleASHA512SameFile", data: []byte{'a'}, hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: true, - expectedHash: []byte{232, 90, 223, 95, 60, 151, 149, 172, 174, 58, 206, 97, 189, 103, 6, 202, 67, 248, 1, 189, 243, 51, 250, 42, 5, 89, 195, 9, 50, 74, 39, 169, 114, 228, 109, 225, 128, 210, 63, 94, 18, 133, 58, 48, 225, 100, 176, 55, 87, 60, 235, 224, 143, 41, 15, 253, 94, 28, 251, 233, 99, 207, 152, 108}, + expectedHash: []byte{24, 10, 13, 25, 113, 62, 169, 99, 151, 70, 166, 113, 81, 81, 163, 85, 5, 25, 29, 15, 46, 37, 104, 120, 142, 218, 52, 178, 187, 83, 30, 166, 101, 87, 70, 196, 188, 61, 123, 20, 13, 254, 126, 52, 212, 111, 75, 203, 33, 233, 233, 47, 181, 161, 43, 193, 131, 41, 99, 33, 164, 73, 89, 152}, }, { name: "OnePageASHA256SeparateFile", data: bytes.Repeat([]byte{'a'}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: false, - expectedHash: []byte{157, 60, 139, 54, 248, 39, 187, 77, 31, 107, 241, 26, 240, 49, 83, 159, 182, 60, 128, 85, 121, 204, 15, 249, 44, 248, 127, 134, 58, 220, 41, 185}, + expectedHash: []byte{132, 54, 112, 142, 156, 19, 50, 140, 138, 240, 192, 154, 100, 120, 242, 69, 64, 217, 62, 166, 127, 88, 23, 197, 100, 66, 255, 215, 214, 229, 54, 1}, }, { name: "OnePageASHA256SameFile", data: bytes.Repeat([]byte{'a'}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: true, - expectedHash: []byte{157, 60, 139, 54, 248, 39, 187, 77, 31, 107, 241, 26, 240, 49, 83, 159, 182, 60, 128, 85, 121, 204, 15, 249, 44, 248, 127, 134, 58, 220, 41, 185}, + expectedHash: []byte{132, 54, 112, 142, 156, 19, 50, 140, 138, 240, 192, 154, 100, 120, 242, 69, 64, 217, 62, 166, 127, 88, 23, 197, 100, 66, 255, 215, 214, 229, 54, 1}, }, { name: "OnePageASHA512SeparateFile", data: bytes.Repeat([]byte{'a'}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: false, - expectedHash: []byte{116, 22, 252, 100, 32, 241, 254, 228, 167, 228, 110, 146, 156, 189, 6, 30, 27, 127, 94, 181, 15, 98, 173, 60, 34, 102, 92, 174, 181, 80, 205, 90, 88, 12, 125, 194, 148, 175, 184, 168, 37, 66, 127, 194, 19, 132, 93, 147, 168, 217, 227, 131, 100, 25, 213, 255, 132, 60, 196, 217, 24, 158, 1, 50}, + expectedHash: []byte{165, 46, 176, 116, 47, 209, 101, 193, 64, 185, 30, 9, 52, 22, 24, 154, 135, 220, 232, 168, 215, 45, 222, 226, 207, 104, 160, 10, 156, 98, 245, 250, 76, 21, 68, 204, 65, 118, 69, 52, 210, 155, 36, 109, 233, 103, 1, 40, 218, 89, 125, 38, 247, 194, 2, 225, 119, 155, 65, 99, 182, 111, 110, 145}, }, { name: "OnePageASHA512SameFile", data: bytes.Repeat([]byte{'a'}, usermem.PageSize), hashAlgorithms: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: true, - expectedHash: []byte{116, 22, 252, 100, 32, 241, 254, 228, 167, 228, 110, 146, 156, 189, 6, 30, 27, 127, 94, 181, 15, 98, 173, 60, 34, 102, 92, 174, 181, 80, 205, 90, 88, 12, 125, 194, 148, 175, 184, 168, 37, 66, 127, 194, 19, 132, 93, 147, 168, 217, 227, 131, 100, 25, 213, 255, 132, 60, 196, 217, 24, 158, 1, 50}, + expectedHash: []byte{165, 46, 176, 116, 47, 209, 101, 193, 64, 185, 30, 9, 52, 22, 24, 154, 135, 220, 232, 168, 215, 45, 222, 226, 207, 104, 160, 10, 156, 98, 245, 250, 76, 21, 68, 204, 65, 118, 69, 52, 210, 155, 36, 109, 233, 103, 1, 40, 218, 89, 125, 38, 247, 194, 2, 225, 119, 155, 65, 99, 182, 111, 110, 145}, }, } @@ -348,9 +349,9 @@ func TestGenerate(t *testing.T) { // prepareVerify generates test data and corresponding Merkle tree, and returns // the prepared VerifyParams. -// The test data has size dataSize. The data is hashed with hashAlgorithms. The -// portion to be verified ranges from verifyStart with verifySize. -func prepareVerify(t *testing.T, dataSize int64, hashAlgorithm int, dataAndTreeInSameFile bool, verifyStart int64, verifySize int64, out io.Writer) ([]byte, VerifyParams) { +// The test data has size dataSize. The data is hashed with hashAlgorithm. The +// portion to be verified is the range [verifyStart, verifyStart + verifySize). +func prepareVerify(t *testing.T, dataSize int64, hashAlgorithm int, dataAndTreeInSameFile, isSymlink bool, verifyStart, verifySize int64, out io.Writer) ([]byte, VerifyParams) { t.Helper() data := make([]byte, dataSize) // Generate random bytes in data. @@ -377,6 +378,10 @@ func prepareVerify(t *testing.T, dataSize int64, hashAlgorithm int, dataAndTreeI bytes: data, } } + + if isSymlink { + genParams.SymlinkTarget = defaultSymlinkPath + } hash, err := Generate(&genParams) if err != nil { t.Fatalf("could not generate Merkle tree:%v", err) @@ -428,7 +433,7 @@ func TestVerifyInvalidRange(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, linux.FS_VERITY_HASH_ALG_SHA256, false /* dataAndTreeInSameFile */, tc.verifyStart, tc.verifySize, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, linux.FS_VERITY_HASH_ALG_SHA256, false /* dataAndTreeInSameFile */, false /* isSymlink */, tc.verifyStart, tc.verifySize, &buf) if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") } @@ -436,37 +441,46 @@ func TestVerifyInvalidRange(t *testing.T) { } } +// TODO(b/179422935): Cleanup merkletree verify tests. func TestVerifyUnmodifiedMetadata(t *testing.T) { testCases := []struct { name string hashAlgorithm int dataAndTreeInSameFile bool + isSymlink bool }{ { name: "SHA256SeparateFile", hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: false, + isSymlink: true, }, { name: "SHA512SeparateFile", hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: false, + isSymlink: false, }, { name: "SHA256SameFile", hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA256, dataAndTreeInSameFile: true, + isSymlink: false, }, { name: "SHA512SameFile", hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA512, dataAndTreeInSameFile: true, + isSymlink: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, tc.isSymlink, 0 /* verifyStart */, 0 /* verifySize */, &buf) + if tc.isSymlink { + params.SymlinkTarget = defaultSymlinkPath + } if _, err := Verify(¶ms); !errors.Is(err, nil) { t.Errorf("Verification failed when expected to succeed: %v", err) } @@ -504,7 +518,7 @@ func TestVerifyModifiedName(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.Name += "abc" if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -543,7 +557,7 @@ func TestVerifyModifiedSize(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.Size-- if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -582,7 +596,7 @@ func TestVerifyModifiedMode(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.Mode++ if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -621,7 +635,7 @@ func TestVerifyModifiedUID(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.UID++ if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -660,7 +674,7 @@ func TestVerifyModifiedGID(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.GID++ if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -699,7 +713,7 @@ func TestVerifyModifiedChildren(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer - _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, 0 /* verifyStart */, 0 /* verifySize */, &buf) + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) params.Children["abc"] = struct{}{} if _, err := Verify(¶ms); errors.Is(err, nil) { t.Errorf("Verification succeeded when expected to fail") @@ -708,6 +722,45 @@ func TestVerifyModifiedChildren(t *testing.T) { } } +func TestVerifyModifiedSymlink(t *testing.T) { + testCases := []struct { + name string + hashAlgorithm int + dataAndTreeInSameFile bool + }{ + { + name: "SHA256SeparateFile", + hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA256, + dataAndTreeInSameFile: false, + }, + { + name: "SHA512SeparateFile", + hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA512, + dataAndTreeInSameFile: false, + }, + { + name: "SHA256SameFile", + hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA256, + dataAndTreeInSameFile: true, + }, + { + name: "SHA512SameFile", + hashAlgorithm: linux.FS_VERITY_HASH_ALG_SHA512, + dataAndTreeInSameFile: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + _, params := prepareVerify(t, usermem.PageSize /* dataSize */, tc.hashAlgorithm, tc.dataAndTreeInSameFile, true /* isSymlink */, 0 /* verifyStart */, 0 /* verifySize */, &buf) + params.SymlinkTarget = "merkle_modified_test_link" + if _, err := Verify(¶ms); err == nil { + t.Errorf("Verification succeeded when expected to fail") + } + }) + } +} + func TestModifyOutsideVerifyRange(t *testing.T) { testCases := []struct { name string @@ -772,7 +825,7 @@ func TestModifyOutsideVerifyRange(t *testing.T) { verifySize := int64(usermem.PageSize) var buf bytes.Buffer // Modified byte is outside verify range. Verify should succeed. - data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, verifyStart, verifySize, &buf) + data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, verifyStart, verifySize, &buf) // Flip a bit in data and checks Verify results. data[tc.modifyByte] ^= 1 n, err := Verify(¶ms) @@ -1015,7 +1068,7 @@ func TestModifyInsideVerifyRange(t *testing.T) { t.Run(tc.name, func(t *testing.T) { dataSize := int64(8 * usermem.PageSize) var buf bytes.Buffer - data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, tc.verifyStart, tc.verifySize, &buf) + data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, tc.verifyStart, tc.verifySize, &buf) // Flip a bit in data and checks Verify results. data[tc.modifyByte] ^= 1 if _, err := Verify(¶ms); errors.Is(err, nil) { @@ -1064,7 +1117,7 @@ func TestVerifyRandom(t *testing.T) { size := rand.Int63n(dataSize) + 1 var buf bytes.Buffer - data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, start, size, &buf) + data, params := prepareVerify(t, dataSize, tc.hashAlgorithm, tc.dataAndTreeInSameFile, false /* isSymlink */, start, size, &buf) // Checks that the random portion of data from the original data is // verified successfully. 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 |