fflash: Add flag to skip disabling rootfs verification

This is useful on the rare occurrence to do testing on readonly rootfs.

BUG=None
TEST=go test ./...
TEST=ninja
TEST=bin/integration-test $dut

Change-Id: If63c2878289c8b85969959f41deaf7384ee28ec5
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3947886
Tested-by: Li-Yu Yu <aaronyu@google.com>
Reviewed-by: Terry Cheong <htcheong@chromium.org>
Commit-Queue: Li-Yu Yu <aaronyu@google.com>
diff --git a/contrib/fflash/cmd/integration-test/main.go b/contrib/fflash/cmd/integration-test/main.go
index 7a61492..f8a29cc 100644
--- a/contrib/fflash/cmd/integration-test/main.go
+++ b/contrib/fflash/cmd/integration-test/main.go
@@ -41,16 +41,22 @@
 		log.Fatal("failed to verify junk after writing:", err)
 	}
 
-	// Flash without clobbering.
-	if err := internal.CLIMain(ctx, t0, []string{target}); err != nil {
+	// Flash without clobbering, with rootfs verification.
+	if err := internal.CLIMain(ctx, t0, []string{target, "--rootfs-verification=yes"}); err != nil {
 		log.Fatal("non-clobbering flash failed:", err)
 	}
 
+	// Check rootfs verification is not disabled.
+	if err := checkRootfsVerification(ctx, target, true); err != nil {
+		log.Fatal("check for enabled rootfs verification failed: ", err)
+	}
+
+	// Check the junk file is still there.
 	if err := verifyJunk(ctx, target); err != nil {
 		log.Fatal("failed to verify junk after non-clobbering flash:", err)
 	}
 
-	// Flash again with clobber
+	// Flash again with clobber, without rootfs verification.
 	if err := internal.CLIMain(ctx, t0, []string{target, "--clobber-stateful=yes"}); err != nil {
 		log.Fatal("clobbering flash failed:", err)
 	}
@@ -62,6 +68,11 @@
 		log.Fatal("junk file is not removed after clobbering flash")
 	}
 
+	// Check rootfs verification is disabled.
+	if err := checkRootfsVerification(ctx, target, false); err != nil {
+		log.Fatal("check for disabled rootfs verification failed: ", err)
+	}
+
 	log.Println("integration test complete")
 }
 
@@ -90,3 +101,22 @@
 
 	return nil
 }
+
+// checkRootfsVerification returns whether rootfs verification is in the desired state.
+func checkRootfsVerification(ctx context.Context, target string, enabled bool) error {
+	cmd := ssh.DefaultCommand(ctx)
+	cmd.Args = append(cmd.Args, target,
+		// Check that rootfs verification is disabled.
+		"/usr/libexec/debugd/helpers/dev_features_rootfs_verification", "-q",
+	)
+	if enabled {
+		// Invert the exit status. Doing so in shell allows us to distinguish
+		// ssh failures from dev_features_rootfs_verification returning non-zero.
+		//
+		// If dev_features_rootfs_verification fails, then exit 0 is executed.
+		// If dev_features_rootfs_verification succeeds, then exit 0 is skipped, exit 1 is executed.
+		cmd.Args = append(cmd.Args, "||", "exit", "0", "&&", "exit", "1")
+	}
+
+	return cmd.Run()
+}
diff --git a/contrib/fflash/internal/cli.go b/contrib/fflash/internal/cli.go
index 83b1c5a..273460a 100644
--- a/contrib/fflash/internal/cli.go
+++ b/contrib/fflash/internal/cli.go
@@ -28,6 +28,11 @@
 	app.Flag("board",
 		"flash from gs://chromeos-image-archive/${board}-release/R*. Use with caution!").
 		StringVar(&opts.Board)
+	rootfsVerification := app.Flag(
+		"rootfs-verification",
+		"whether rootfs verification on the new root is enabled. "+
+			"Choices: yes, no (default)",
+	).Default(no).Enum(yes, no)
 	clobberStateful := app.Flag(
 		"clobber-stateful",
 		"whether to clobber the stateful partition. Choices: yes, no (default)").Default(no).Enum(yes, no)
@@ -46,6 +51,7 @@
 		opts.ReleaseNum = r
 		opts.ReleaseString = ""
 	}
+	opts.DisableRootfsVerification = (*rootfsVerification == no)
 	opts.ClobberStateful = (*clobberStateful == yes)
 	if *clearTpmOwner == auto {
 		opts.ClearTpmOwner = opts.ClobberStateful
diff --git a/contrib/fflash/internal/cli_test.go b/contrib/fflash/internal/cli_test.go
index 4026d6c..6523300 100644
--- a/contrib/fflash/internal/cli_test.go
+++ b/contrib/fflash/internal/cli_test.go
@@ -23,15 +23,20 @@
 		"defaults": {
 			args:   []string{"dut"},
 			target: "dut",
-			opts:   Options{},
+			opts: Options{
+				FlashOptions: dut.FlashOptions{
+					DisableRootfsVerification: true,
+				},
+			},
 		},
 		"clobber": {
 			args:   []string{"dut", "--clobber-stateful=yes"},
 			target: "dut",
 			opts: Options{
 				FlashOptions: dut.FlashOptions{
-					ClobberStateful: true,
-					ClearTpmOwner:   true, // follows --clobber-stateful by default
+					DisableRootfsVerification: true,
+					ClobberStateful:           true,
+					ClearTpmOwner:             true, // follows --clobber-stateful by default
 				},
 			},
 		},
@@ -40,8 +45,18 @@
 			target: "dut",
 			opts: Options{
 				FlashOptions: dut.FlashOptions{
-					ClobberStateful: true,
-					ClearTpmOwner:   false,
+					DisableRootfsVerification: true,
+					ClobberStateful:           true,
+					ClearTpmOwner:             false,
+				},
+			},
+		},
+		"disable-rootfs-verification": {
+			args:   []string{"dut", "--rootfs-verification=yes"},
+			target: "dut",
+			opts: Options{
+				FlashOptions: dut.FlashOptions{
+					DisableRootfsVerification: false,
 				},
 			},
 		},
diff --git a/contrib/fflash/internal/dut/main.go b/contrib/fflash/internal/dut/main.go
index d92706c..8119154 100644
--- a/contrib/fflash/internal/dut/main.go
+++ b/contrib/fflash/internal/dut/main.go
@@ -87,10 +87,12 @@
 
 	result := &Result{}
 
-	log.Println("disabling rootfs verification")
-	if err := DisableRootfsVerification(ctx, partState.InactiveKernelNum); err != nil {
-		log.Printf("disable rootfs verification failed (will retry after reboot): %s", err)
-		result.RetryDisableRootfsVerification = true
+	if r.DisableRootfsVerification {
+		log.Println("disabling rootfs verification")
+		if err := DisableRootfsVerification(ctx, partState.InactiveKernelNum); err != nil {
+			log.Printf("disable rootfs verification failed (will retry after reboot): %s", err)
+			result.RetryDisableRootfsVerification = true
+		}
 	}
 
 	if r.ClearTpmOwner {
diff --git a/contrib/fflash/internal/dut/request.go b/contrib/fflash/internal/dut/request.go
index 620c251..b0bbd42 100644
--- a/contrib/fflash/internal/dut/request.go
+++ b/contrib/fflash/internal/dut/request.go
@@ -26,8 +26,9 @@
 // Unlike Request.Bucket, Request.Directory, these are determined solely by
 // parsing the command line without further processing.
 type FlashOptions struct {
-	ClobberStateful bool // whether to clobber the stateful partition
-	ClearTpmOwner   bool // whether to clean tpm owner on reboot
+	DisableRootfsVerification bool // whether to disable rootfs verification
+	ClobberStateful           bool // whether to clobber the stateful partition
+	ClearTpmOwner             bool // whether to clean tpm owner on reboot
 }
 
 type Result struct {
diff --git a/contrib/fflash/internal/main.go b/contrib/fflash/internal/main.go
index 1d05851..cea1b98 100644
--- a/contrib/fflash/internal/main.go
+++ b/contrib/fflash/internal/main.go
@@ -207,8 +207,10 @@
 		}
 	}
 
-	if _, err := sshClient.RunSimpleOutput(devFeaturesRootfsVerification + " -q"); err != nil {
-		return fmt.Errorf("failed to check rootfs verification: %w", err)
+	if opts.DisableRootfsVerification {
+		if _, err := sshClient.RunSimpleOutput(devFeaturesRootfsVerification + " -q"); err != nil {
+			return fmt.Errorf("failed to check rootfs verification: %w", err)
+		}
 	}
 
 	return nil