| // Copyright 2019 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package verity provides variation of dm-verity tests and its utilities. |
| package verity |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "time" |
| |
| "chromiumos/tast/common/testexec" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/testing" |
| ) |
| |
| const ( |
| blockSize = 4096 |
| |
| // The number of blocks of the test image file. |
| nTestBlocks = 100 |
| ) |
| |
| // releaseDevice releases the device by executing the given command. |
| // Note that there could be a chance some other thing keeps the device open. |
| // Considering the case, retries for 1 sec. |
| func releaseDevice(ctx context.Context, c []string, device string) error { |
| err := testing.Poll(ctx, func(ctx context.Context) error { |
| if _, err := os.Stat(device); os.IsNotExist(err) { |
| // File is already removed. |
| return nil |
| } |
| return testexec.CommandContext(ctx, c[0], c[1:]...).Run() |
| }, &testing.PollOptions{Timeout: time.Second * 2}) |
| if err == nil { |
| // Succeeded. |
| return nil |
| } |
| |
| testing.ContextLogf(ctx, "Failed release command %s for device %s: %s", |
| c[0], device, err) |
| if _, err := os.Stat(device); os.IsNotExist(err) { |
| // File was removed during timeout. |
| return nil |
| } |
| |
| // Build an error message useful for debugging. |
| cmd := testexec.CommandContext(ctx, "fuser", "-v", device) |
| fuser, ferr := cmd.Output() |
| if ferr != nil { |
| // fuser is just for logging purpose, so ignore an error. |
| testing.ContextLog(ctx, "Failed to call fuser: ", ferr) |
| cmd.DumpLog(ctx) |
| } |
| |
| cmd = testexec.CommandContext(ctx, "lsblk", device) |
| lsblk, lerr := cmd.Output() |
| if lerr != nil { |
| // lsblk is just for logging purpose, so ignore an error. |
| testing.ContextLog(ctx, "Failed to call lsblk: ", lerr) |
| cmd.DumpLog(ctx) |
| } |
| |
| return errors.Wrapf(err, "failed to release %s, fuser=%s, lsblk=%s", device, fuser, lsblk) |
| } |
| |
| // createImage creates a temporary file for testing under dir. |
| // The size of the file will be nBlocks * blockSize bytes of 0. |
| // The path to the created image is returned. Callers have responsibility to |
| // delete the file when it gets no longer needed. |
| func createImage(ctx context.Context, dir, name string, nBlocks uint) (string, error) { |
| f, err := ioutil.TempFile(dir, name+".img.") |
| if err != nil { |
| return "", err |
| } |
| defer func() { |
| if f != nil { |
| os.Remove(f.Name()) |
| } |
| }() |
| if err := f.Close(); err != nil { |
| return "", err |
| } |
| cmd := testexec.CommandContext( |
| ctx, "dd", "if=/dev/zero", "of="+f.Name(), |
| fmt.Sprintf("bs=%d", blockSize), "count=0", |
| fmt.Sprintf("seek=%d", nBlocks)) |
| if err := cmd.Run(); err != nil { |
| cmd.DumpLog(ctx) |
| return "", err |
| } |
| ret := f.Name() |
| f = nil |
| return ret, nil |
| } |
| |
| // createFileSystem creates a file system on path. |
| func createFileSystem(ctx context.Context, path string) error { |
| cmd := testexec.CommandContext( |
| ctx, "mkfs.ext3", "-b", strconv.Itoa(blockSize), "-F", path) |
| if err := cmd.Run(); err != nil { |
| cmd.DumpLog(ctx) |
| return err |
| } |
| return nil |
| } |
| |
| // createHash generates the hash for verity. On success, the path to the hash |
| // file and the device mapper table are returned. |
| func createHash(ctx context.Context, dir, name, image string, nBlocks uint) (hash, table string, err error) { |
| f, err := ioutil.TempFile(dir, name+".hash.") |
| if err != nil { |
| return "", "", err |
| } |
| defer func() { |
| if f != nil { |
| os.Remove(f.Name()) |
| } |
| }() |
| if err := f.Close(); err != nil { |
| return "", "", err |
| } |
| cmd := testexec.CommandContext( |
| ctx, "verity", "mode=create", "alg=sha256", |
| "payload="+image, fmt.Sprintf("payload_blocks=%d", nBlocks), |
| "hashtree="+f.Name()) |
| out, err := cmd.Output() |
| if err != nil { |
| cmd.DumpLog(ctx) |
| return "", "", err |
| } |
| ret := f.Name() |
| f = nil |
| return ret, strings.TrimSpace(string(out)), nil |
| } |
| |
| // appendHash appends the contents of the hash file to the image file. |
| func appendHash(image, hash string) error { |
| content, err := ioutil.ReadFile(hash) |
| if err != nil { |
| return err |
| } |
| file, err := os.OpenFile(image, os.O_WRONLY|os.O_APPEND, 0666) |
| if err != nil { |
| return err |
| } |
| defer func() { |
| if file != nil { |
| file.Close() |
| } |
| }() |
| if _, err := file.WriteString(string(content)); err != nil { |
| return err |
| } |
| err = file.Close() |
| file = nil |
| return err |
| } |
| |
| // setUpLoop sets up the loopback device for the given image, and returns |
| // its path. |
| func setUpLoop(ctx context.Context, image string) (string, error) { |
| cmd := testexec.CommandContext(ctx, "losetup", "-f", "--show", image) |
| out, err := cmd.Output() |
| if err != nil { |
| cmd.DumpLog(ctx) |
| return "", err |
| } |
| return strings.TrimSpace(string(out)), nil |
| } |
| |
| // tearDownLoop tears down the loopback device created by setUpLoop. |
| func tearDownLoop(ctx context.Context, loop string) error { |
| return releaseDevice(ctx, []string{"losetup", "-d", loop}, loop) |
| } |
| |
| // createVerityDevice sets up the verity device for the given loopback |
| // device with the name and device table, and returns its path. |
| func createVerityDevice(ctx context.Context, name, loop, table string) (string, error) { |
| devName := "tast_" + name |
| devPath := "/dev/mapper/" + devName |
| |
| // Clean up stale device file, if exists. |
| // Ignore errors, which could be reported in clean state. |
| // Force and retry the command to give process that maybe using |
| // the device time to close. |
| testexec.CommandContext(ctx, "dmsetup", "remove", "--force", "--retry", devPath).Run() |
| // Using status to check that the volume is removed. If the volume |
| // is found, the following tests will fail. |
| cmd := testexec.CommandContext(ctx, "dmsetup", "status", devName) |
| if cmd.Run() == nil { |
| cmd.DumpLog(ctx) |
| return "", errors.New("failed to remove volume") |
| } |
| |
| table = strings.Replace(table, "HASH_DEV", loop, 1) |
| table = strings.Replace(table, "ROOT_DEV", loop, 1) |
| table = table + " error_behavior=eio" |
| |
| if err := testexec.CommandContext(ctx, "dmsetup", "-r", "create", devName, "--table", table).Run(testexec.DumpLogOnError); err != nil { |
| return "", errors.Wrap(err, "failed to create volume") |
| } |
| |
| if err := testexec.CommandContext(ctx, "dmsetup", "status", devName).Run(testexec.DumpLogOnError); err != nil { |
| return "", errors.Wrap(err, "failed to find volume") |
| } |
| |
| return devPath, nil |
| } |
| |
| // removeVerityDevice tears down the verity device created by the |
| // createVerityDevice. |
| func removeVerityDevice(ctx context.Context, device string) error { |
| return releaseDevice(ctx, []string{"dmsetup", "remove", "--force", "--retry", device}, device) |
| } |
| |
| // verifiable walks completely over the device, and returns any error if |
| // found. |
| func verifiable(ctx context.Context, device string) error { |
| cmd := testexec.CommandContext(ctx, "dumpe2fs", device) |
| if err := cmd.Run(); err != nil { |
| return errors.Wrap(err, "failed to run dumpe2fs") |
| } |
| |
| cmd = testexec.CommandContext(ctx, "dd", "if="+device, "of=/dev/null", |
| fmt.Sprintf("bs=%d", blockSize)) |
| if err := cmd.Run(); err != nil { |
| return errors.Wrap(err, "failed to read device") |
| } |
| return nil |
| } |
| |
| // checkTools returns whether the dm-verity related test can run on the current |
| // environment. |
| func checkTools() []error { |
| var required = []string{ |
| "losetup", |
| "mkfs.ext3", |
| "dmsetup", |
| "verity", |
| "dd", |
| "dumpe2fs", |
| } |
| |
| var missing []error |
| for _, tool := range required { |
| if _, err := exec.LookPath(tool); err != nil { |
| missing = append(missing, err) |
| } |
| } |
| return missing |
| } |
| |
| // runCheck sets up the testing environment (specifically sets up device), |
| // call modify with the created image file, verifies dm-verify behavior |
| // then tears down the device. |
| // Returns error if some setup/teardown fails, or the dm-verify behavior is |
| // different from expect (true on success, false on fail). |
| func runCheck(ctx context.Context, name string, expect bool, modify func(string) error) error { |
| dir, err := ioutil.TempDir("", "") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(dir) |
| |
| image, err := createImage(ctx, dir, name, nTestBlocks) |
| if err != nil { |
| return err |
| } |
| if err := createFileSystem(ctx, image); err != nil { |
| return err |
| } |
| hash, table, err := createHash(ctx, dir, name, image, nTestBlocks) |
| if err != nil { |
| return err |
| } |
| // The hash size should be blockSize. |
| if fi, err := os.Stat(hash); err != nil { |
| return err |
| } else if fi.Size() != blockSize { |
| return errors.Errorf("unexpected hash file size for %s: got %d; want %d", hash, fi.Size(), blockSize) |
| } |
| |
| if err := appendHash(image, hash); err != nil { |
| return err |
| } |
| loop, err := setUpLoop(ctx, image) |
| if err != nil { |
| return err |
| } |
| defer tearDownLoop(ctx, loop) |
| |
| if err = modify(image); err != nil { |
| return err |
| } |
| |
| // Create a random device name using the temporary file generated for the image. |
| // We see delays in volume cleanup that cause some flakyness in VM tests. Using |
| // random unique name removes the problems with delayed cleanup. |
| dev, err := createVerityDevice(ctx, fmt.Sprintf("%s_%s", filepath.Base(dir), name), loop, table) |
| if err != nil { |
| return err |
| } |
| defer removeVerityDevice(ctx, dev) |
| |
| err = verifiable(ctx, dev) |
| if expect && err != nil { |
| return errors.Wrap(err, "unexpected verifiable failure") |
| } else if !expect && err == nil { |
| return errors.New("unexpected verifiable success") |
| } |
| |
| return nil |
| } |
| |
| func testNoModify(ctx context.Context, s *testing.State) { |
| s.Log("Running testNoModify") |
| |
| if err := runCheck(ctx, "NoModify", true, func(image string) error { return nil }); err != nil { |
| s.Error("NoModify test failed: ", err) |
| } |
| } |
| |
| func testZeroFill(ctx context.Context, s *testing.State) { |
| s.Log("Running testZeroFill") |
| |
| // Test 0-filled data for each block. |
| for i := 0; i < nTestBlocks; i++ { |
| if err := runCheck(ctx, "ZeroFill", false, func(image string) error { |
| return testexec.CommandContext( |
| ctx, "dd", "if=/dev/zero", "of="+image, |
| fmt.Sprintf("bs=%d", blockSize), |
| fmt.Sprintf("seek=%d", i), "count=1").Run() |
| }); err != nil { |
| s.Errorf("ZeroFill test failed at blocks=%d: %v", i, err) |
| return |
| } |
| } |
| } |
| |
| func testAFill(ctx context.Context, s *testing.State) { |
| s.Log("Running testAFill") |
| |
| // Fill hash section of the image by consecutive "A" bytes. |
| if err := runCheck(ctx, "AFill", false, func(image string) error { |
| f, err := os.OpenFile(image, os.O_WRONLY, 0666) |
| if err != nil { |
| return err |
| } |
| if _, err := f.WriteAt(bytes.Repeat([]byte("A"), blockSize), nTestBlocks*blockSize); err != nil { |
| return err |
| } |
| return nil |
| }); err != nil { |
| s.Error("AFill test failed: ", err) |
| } |
| } |
| |
| // testBitFlip exercises the bit flip for each block. |
| func testBitFlip(ctx context.Context, s *testing.State, off int64, mask byte) { |
| // Walks nTestBlocks followed by a hash block. |
| for i := 0; i < nTestBlocks+1; i++ { |
| if err := runCheck(ctx, "BitFlip", false, func(image string) error { |
| f, err := os.OpenFile(image, os.O_RDWR, 0666) |
| if err != nil { |
| return err |
| } |
| pos := int64(i*blockSize) + off |
| buf := make([]byte, 1) |
| if _, err = f.ReadAt(buf, pos); err != nil { |
| return err |
| } |
| buf[0] ^= mask |
| if _, err = f.WriteAt(buf, pos); err != nil { |
| return err |
| } |
| return nil |
| }); err != nil { |
| s.Errorf("BitFlip test failed at blocks=%d: %v", i, err) |
| return |
| } |
| } |
| } |
| |
| func testFirstBitFlip(ctx context.Context, s *testing.State) { |
| s.Log("Running testFirstBitFlip") |
| testBitFlip(ctx, s, 0, 0x80) |
| } |
| |
| func testMiddleBitFlip(ctx context.Context, s *testing.State) { |
| s.Log("Running testMiddleBitFlip") |
| testBitFlip(ctx, s, blockSize/2, 0x80) |
| } |
| |
| func testLastBitFlip(ctx context.Context, s *testing.State) { |
| s.Log("Running testLastBitFlip") |
| testBitFlip(ctx, s, blockSize-1, 0x01) |
| } |
| |
| // RunTests runs a series of tests related to dm-verity. |
| func RunTests(ctx context.Context, s *testing.State) { |
| if errs := checkTools(); errs != nil { |
| for _, err := range errs { |
| s.Error("Tool not found: ", err) |
| } |
| return |
| } |
| |
| testNoModify(ctx, s) |
| testZeroFill(ctx, s) |
| testAFill(ctx, s) |
| testFirstBitFlip(ctx, s) |
| testMiddleBitFlip(ctx, s) |
| testLastBitFlip(ctx, s) |
| } |