blob: eded13e43cd584f80c0cbe6ad1552b007bd6aee3 [file] [log] [blame]
// Copyright 2021 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 firmware
import (
"context"
"fmt"
"path/filepath"
"regexp"
"strconv"
"time"
"chromiumos/tast/common/servo"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/remote/firmware"
"chromiumos/tast/remote/firmware/fixture"
"chromiumos/tast/remote/firmware/reporters"
pb "chromiumos/tast/services/cros/firmware"
"chromiumos/tast/ssh"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
// Create enum to specify which tests need to be run
type wpTestRebootMethod int
const (
rebootWithModeAwareReboot wpTestRebootMethod = iota
rebootWithShutdownCmd
rebootWithRebootCmd
rebootWithPowerBtn
)
func init() {
testing.AddTest(&testing.Test{
Func: WriteProtect,
Desc: "Verify enabling and disabling write protect works as expected",
Contacts: []string{"tij@google.com", "cros-fw-engprod@google.com"},
Attr: []string{"group:firmware", "firmware_unstable"},
SoftwareDeps: []string{"crossystem", "flashrom"},
ServiceDeps: []string{"tast.cros.firmware.BiosService"},
HardwareDeps: hwdep.D(hwdep.ChromeEC()),
Timeout: 20 * time.Minute,
Params: []testing.Param{
{
Name: "dev_mode_with_mode_aware_reboot",
Fixture: fixture.DevMode,
Val: rebootWithModeAwareReboot,
},
{
Name: "normal_mode_with_mode_aware_reboot",
Fixture: fixture.NormalMode,
Val: rebootWithModeAwareReboot,
},
{
Name: "normal_mode_with_shutdown_cmd",
Fixture: fixture.NormalMode,
Val: rebootWithShutdownCmd,
},
{
Name: "normal_mode_with_reboot_cmd",
Fixture: fixture.NormalMode,
Val: rebootWithRebootCmd,
},
{
Name: "normal_mode_with_power_btn",
Fixture: fixture.NormalMode,
Val: rebootWithPowerBtn,
},
},
})
}
const (
shutdownTimeout time.Duration = 2 * time.Second
rebootTimeout time.Duration = 5 * time.Second
)
var (
reFlashromSuccess = regexp.MustCompile(`SUCCESS`)
reFlashromFail = regexp.MustCompile(`FAILED`)
reWPROfmap = regexp.MustCompile(`WP_RO\s+(\d+)\s+(\d+)`)
reROFRIDfmap = regexp.MustCompile(`RO_FRID\s+(\d+)\s+(\d+)`)
)
type wpTarget string
const (
targetBIOS wpTarget = "bios"
targetEC wpTarget = "ec"
)
var flashromTargets = map[wpTarget]string{
targetBIOS: "host",
targetEC: "ec",
}
var tmpDirPath = filepath.Join("/", "mnt", "stateful_partition", fmt.Sprintf("flashrom_%d", time.Now().Unix()))
func WriteProtect(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
rebootMethod := s.Param().(wpTestRebootMethod)
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to connect to servo: ", err)
}
if err := h.RequireBiosServiceClient(ctx); err != nil {
s.Fatal("Requiring BiosServiceClient: ", err)
}
// Back up EC_RW.
s.Log("Back up current EC_RW region")
ecrwPath, err := h.BiosServiceClient.BackupImageSection(ctx, &pb.FWBackUpSection{Section: pb.ImageSection_ECRWImageSection, Programmer: pb.Programmer_ECProgrammer})
if err != nil {
s.Fatal("Failed to backup current EC_RW region: ", err)
}
s.Log("EC_RW region backup is stored at: ", ecrwPath.Path)
s.Log("Create temp dir in DUT")
if _, err = h.DUT.Conn().CommandContext(ctx, "mkdir", "-p", tmpDirPath).Output(ssh.DumpLogOnError); err != nil {
s.Fatal("Failed to create temp dir: ", err)
}
cleanupContext := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Minute)
defer cancel()
defer func(ctx context.Context) {
s.Log("Reset write protect to false")
if err := setWriteProtect(ctx, h, targetEC, false); err != nil {
s.Fatal("Failed to set FW write protect state: ", err)
}
// Require again here since reboots in test cause nil pointer errors otherwise.
if err := h.RequireBiosServiceClient(ctx); err != nil {
s.Fatal("Requiring BiosServiceClient: ", err)
}
// Restore EC_RW.
s.Log("Restore EC_RW region with backup from: ", ecrwPath.Path)
if _, err := h.BiosServiceClient.RestoreImageSection(ctx, ecrwPath); err != nil {
s.Fatal("Failed to restore EC_RW image: ", err)
}
s.Log("Delete EC backup")
if _, err := h.DUT.Conn().CommandContext(ctx, "rm", ecrwPath.Path).Output(ssh.DumpLogOnError); err != nil {
s.Fatal("Failed to delete ec backup: ", err)
}
// Clean up temp directory.
s.Log("Delete temp dir and contained files from DUT")
if _, err := h.DUT.Conn().CommandContext(ctx, "rm", "-r", tmpDirPath).Output(ssh.DumpLogOnError); err != nil {
s.Fatal("Failed to delete temp dir: ", err)
}
}(cleanupContext)
// This call takes ~= 10 mins to complete.
s.Log("Test wp state over reboot on target EC")
if err := testWPOverReboot(ctx, h, targetEC, rebootMethod); err != nil {
s.Fatal("Failed to preserve wp state over reboots: ", err)
}
// This call takes ~= 10 mins to complete.
s.Log("Test wp state over reboot on target BIOS")
if err := testWPOverReboot(ctx, h, targetBIOS, rebootMethod); err != nil {
s.Fatal("Failed to preserve wp state over reboots: ", err)
}
// This call takes ~= 2 mins to complete.
s.Log("Test flashrom read/write with wp on target EC")
if err := testReadWrite(ctx, h, targetEC); err != nil {
s.Fatal("Read/write behaved unexpectedly: ", err)
}
// This call takes ~= 2 mins to complete.
s.Log("Test flashrom read/write with wp on target BIOS")
if err := testReadWrite(ctx, h, targetBIOS); err != nil {
s.Fatal("Read/write behaved unexpectedly: ", err)
}
}
func testWPOverReboot(ctx context.Context, h *firmware.Helper, target wpTarget, testRebootMethod wpTestRebootMethod) error {
var rebootMethod string
var rebootFunc func(context.Context, *firmware.Helper) error
switch testRebootMethod {
case rebootWithModeAwareReboot:
rebootMethod = "mode aware reboot"
rebootFunc = performModeAwareReboot
case rebootWithShutdownCmd:
rebootMethod = "mode aware reboot"
rebootFunc = performRebootWithShutdownCmd
case rebootWithRebootCmd:
rebootMethod = "mode aware reboot"
rebootFunc = performRebootWithRebootCmd
case rebootWithPowerBtn:
rebootMethod = "mode aware reboot"
rebootFunc = performRebootWithPowerBtn
}
testing.ContextLog(ctx, "Enable Write Protect")
if err := setWriteProtect(ctx, h, target, true); err != nil {
return errors.Wrap(err, "failed to enable FW write protect state")
}
testing.ContextLog(ctx, "Reboot DUT using ", rebootMethod)
if err := rebootFunc(ctx, h); err != nil {
return errors.Wrapf(err, "failed to reboot with %q", rebootMethod)
}
testing.ContextLog(ctx, "Expect write protect state to be enabled")
if err := checkCrossystem(ctx, h, 1); err != nil {
return errors.Wrap(err, "failed to check crossystem")
}
testing.ContextLog(ctx, "Disable Write Protect")
if err := setWriteProtect(ctx, h, targetEC, false); err != nil {
return errors.Wrap(err, "failed to disable FW write protect state")
}
testing.ContextLog(ctx, "Reboot DUT using ", rebootMethod)
if err := rebootFunc(ctx, h); err != nil {
return errors.Wrapf(err, "failed to reboot with %q", rebootMethod)
}
testing.ContextLog(ctx, "Expect write protect state to be disabled")
if err := checkCrossystem(ctx, h, 0); err != nil {
return errors.Wrap(err, "failed to check crossystem")
}
return nil
}
func performRebootWithRebootCmd(ctx context.Context, h *firmware.Helper) error {
cmd := h.DUT.Conn().CommandContext(ctx, "reboot")
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to reboot DUT")
}
testing.ContextLog(ctx, "Sleep, wait for DUT to reboot")
if err := testing.Sleep(ctx, rebootTimeout); err != nil {
return errors.Wrapf(err, "failed to sleep for %s after initiating reboot", rebootTimeout)
}
testing.ContextLog(ctx, "Check for S0 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "S0"); err != nil {
return errors.Wrap(err, "failed to get S0 powerstate")
}
return h.WaitConnect(ctx)
}
func performRebootWithShutdownCmd(ctx context.Context, h *firmware.Helper) error {
cmd := h.DUT.Conn().CommandContext(ctx, "/sbin/shutdown", "-P", "now")
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to shut down DUT")
}
testing.ContextLog(ctx, "Sleep, wait for DUT to shutdown")
if err := testing.Sleep(ctx, shutdownTimeout); err != nil {
return errors.Wrapf(err, "failed to sleep for %s after initiating shutdown", shutdownTimeout)
}
testing.ContextLog(ctx, "Check for G3 or S5 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "G3", "S5"); err != nil {
return errors.Wrap(err, "failed to get G3 or S5 powerstate")
}
testing.ContextLog(ctx, "Power DUT back on with short press of the power button")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil {
return errors.Wrap(err, "failed to power on DUT with short press of the power button")
}
testing.ContextLog(ctx, "Check for S0 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "S0"); err != nil {
return errors.Wrap(err, "failed to get S0 powerstate")
}
return h.WaitConnect(ctx)
}
func performRebootWithPowerBtn(ctx context.Context, h *firmware.Helper) error {
testing.ContextLog(ctx, "Power DUT off with long press of the power button")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurLongPress); err != nil {
return errors.Wrap(err, "failed to power on DUT with short press of the power button")
}
testing.ContextLog(ctx, "Sleep, wait for DUT to power off")
if err := testing.Sleep(ctx, rebootTimeout); err != nil {
return errors.Wrapf(err, "failed to sleep for %s after pressing powerbutton", rebootTimeout)
}
testing.ContextLog(ctx, "Check for G3 or S5 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "G3", "S5"); err != nil {
return errors.Wrap(err, "failed to get G3 or S5 powerstate")
}
testing.ContextLog(ctx, "Power DUT back on with short press of the power button")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil {
return errors.Wrap(err, "failed to power on DUT with short press of the power button")
}
testing.ContextLog(ctx, "Check for S0 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "S0"); err != nil {
return errors.Wrap(err, "failed to get S0 powerstate")
}
return h.WaitConnect(ctx)
}
func testReadWrite(ctx context.Context, h *firmware.Helper, target wpTarget) error {
// Current firmware image as read from flash.
roBefore := filepath.Join(tmpDirPath, fmt.Sprintf("%s_ro_before.bin", target))
// Current firmware image with modification to test writing.
roTest := filepath.Join(tmpDirPath, fmt.Sprintf("%s_ro_test.bin", target))
// Firmware as read after writing flash.
roAfter := filepath.Join(tmpDirPath, fmt.Sprintf("%s_ro_after.bin", target))
testing.ContextLog(ctx, "Enable Write Protect")
if err := setWriteProtect(ctx, h, target, true); err != nil {
return errors.Wrap(err, "failed to set FW write protect state")
}
testing.ContextLogf(ctx, "Save current fw image to %q in DUT", roBefore)
out, err := h.DUT.Conn().CommandContext(ctx, "flashrom", "-p", flashromTargets[target], "-r", "-i", fmt.Sprintf("WP_RO:%s", roBefore)).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to run command flashrom")
} else if match := reFlashromSuccess.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce sucess message: %s", string(out))
}
testing.ContextLog(ctx, "Checking fmap")
out, err = h.DUT.Conn().CommandContext(ctx, "dump_fmap", "-p", roBefore).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to dump fmap")
}
// Format for the dumped fmap is "Name offset size".
wproMatch := reWPROfmap.FindSubmatch(out)
if wproMatch == nil {
return errors.New("didn't find WP_RO in fmap")
}
wproOffset, err := strconv.Atoi(string(wproMatch[1]))
if err != nil {
return errors.Wrap(err, "failed to get WP_RO offset as integer")
}
if rofridMatch := reROFRIDfmap.FindSubmatch(out); rofridMatch != nil {
rofridOffset, err := strconv.Atoi(string(rofridMatch[1]))
if err != nil {
return errors.Wrap(err, "failed to get offset int for RO_FRID")
}
rofridSize, err := strconv.Atoi(string(rofridMatch[2]))
if err != nil {
return errors.Wrap(err, "failed to get size int for RO_FRID")
}
if _, err := h.DUT.Conn().CommandContext(ctx, "cp", roBefore, roTest).CombinedOutput(ssh.DumpLogOnError); err != nil {
return errors.Wrapf(err, "failed to copy %q to %q", roBefore, roTest)
}
dd1Args := []string{
fmt.Sprintf("if=%s", roTest), "bs=1",
fmt.Sprintf("count=%d", rofridSize),
fmt.Sprintf("skip=%d", rofridOffset-wproOffset),
}
trArgs := []string{`"[a-zA-Z]"`, `"[A-Za-z]"`}
dd2Args := []string{
fmt.Sprintf("of=%s", roTest), "bs=1",
fmt.Sprintf("count=%d", rofridSize),
fmt.Sprintf("seek=%d", rofridOffset-wproOffset), "conv=notrunc",
}
dd1Cmd := h.DUT.Conn().CommandContext(ctx, "dd", dd1Args...)
trCmd := h.DUT.Conn().CommandContext(ctx, "tr", trArgs...)
dd2Cmd := h.DUT.Conn().CommandContext(ctx, "dd", dd2Args...)
trCmd.Stdin, _ = dd1Cmd.StdoutPipe()
dd2Cmd.Stdin, _ = trCmd.StdoutPipe()
if err := dd2Cmd.Start(); err != nil {
return errors.Wrap(err, "failed to start second dd cmd")
}
if err := trCmd.Start(); err != nil {
return errors.Wrap(err, "failed to start tr cmd")
}
if err := dd1Cmd.Run(); err != nil {
return errors.Wrap(err, "failed to run first dd cmd")
}
if err := trCmd.Wait(); err != nil {
return errors.Wrap(err, "failed to wait for tr cmd to complete")
}
if err := dd2Cmd.Wait(); err != nil {
return errors.Wrap(err, "failed to wait for second dd cmd to complete")
}
} else {
return errors.New("could not find RO_FRID in fmap")
}
testing.ContextLog(ctx, "Attempt to write fw with wp enabled")
out, err = h.DUT.Conn().CommandContext(ctx, "flashrom", "-p", flashromTargets[target], "-w", "-i", fmt.Sprintf("WP_RO:%s", roTest)).CombinedOutput(ssh.DumpLogOnError)
// We expect an error, so err shouldn't be nil, but neither should out. If out is nil, then the error is from an unrelated source.
if out == nil {
return errors.New("flashrom command did not produce any output")
} else if match := reFlashromFail.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce failure message when trying to write: %s", string(out))
}
testing.ContextLog(ctx, "Read fw, make sure write didn't succeed with wp enabled")
out, err = h.DUT.Conn().CommandContext(ctx, "flashrom", "-p", flashromTargets[target], "-r", "-i", fmt.Sprintf("WP_RO:%s", roAfter)).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to run command flashrom")
} else if match := reFlashromSuccess.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce success message when trying to read: %s", string(out))
}
if err := performModeAwareReboot(ctx, h); err != nil {
return errors.Wrap(err, "failed to do a mode aware reboot")
}
out, err = h.DUT.Conn().CommandContext(ctx, "cmp", roBefore, roAfter).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrapf(err, "failed to compare %q and %q using 'cmp'", roBefore, roAfter)
} else if string(out) != "" {
return errors.Wrapf(err, "files %q and %q were not identical, so either write protect or read failed", roBefore, roAfter)
}
testing.ContextLog(ctx, "Disable Write Protect")
if err := setWriteProtect(ctx, h, target, false); err != nil {
return errors.Wrap(err, "failed to set FW write protect state")
}
testing.ContextLog(ctx, "Attempt to write fw with write protect disabled")
out, err = h.DUT.Conn().CommandContext(ctx, "flashrom", "-p", flashromTargets[target], "-w", "-i", fmt.Sprintf("WP_RO:%s", roTest)).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to run command flashrom")
} else if match := reFlashromSuccess.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce success message when trying to write: %s", string(out))
}
testing.ContextLog(ctx, "Read fw, make sure write succeeded with wp disabled")
out, err = h.DUT.Conn().CommandContext(ctx, "flashrom", "-p", flashromTargets[target], "-r", "-i", fmt.Sprintf("WP_RO:%s", roAfter)).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to run command flashrom")
} else if match := reFlashromSuccess.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce success message when trying to read: %s", string(out))
}
out, err = h.DUT.Conn().CommandContext(ctx, "cmp", roTest, roAfter).CombinedOutput(ssh.DumpLogOnError)
if err != nil {
return errors.Wrapf(err, "failed to compare %q and %q using 'cmp'", roTest, roAfter)
} else if string(out) != "" {
return errors.Errorf("Files %q and %q were not identical", roTest, roAfter)
}
return nil
}
func setWriteProtect(ctx context.Context, h *firmware.Helper, target wpTarget, enable bool) error {
enableStr := "enable"
fwwpState := servo.FWWPStateOn
if !enable {
enableStr = "disable"
fwwpState = servo.FWWPStateOff
}
if target == targetBIOS {
// Disable hardware wp for now so flashrom cmd can run.
if err := h.Servo.SetFWWPState(ctx, servo.FWWPStateOff); err != nil {
return errors.Wrap(err, "failed to disable firmware write protect")
}
// This file will get removed when tmpDirPath is removed.
tmpImagePath := filepath.Join(tmpDirPath, "tmp_file.bin")
flashromArgs := []string{
"-p", flashromTargets[target],
"-i", fmt.Sprintf("WP_RO:%s", tmpImagePath),
"--wp-region", "WP_RO",
fmt.Sprintf("--wp-%s", enableStr),
}
out, err := h.DUT.Conn().CommandContext(ctx, "flashrom", flashromArgs...).Output(ssh.DumpLogOnError)
if err != nil {
return errors.Wrapf(err, "failed run flashrom cmd: %s", string(out))
} else if match := reFlashromSuccess.FindSubmatch(out); match == nil {
return errors.Errorf("flashrom did not produce success message when trying to write: %s", string(out))
}
if err := h.Servo.SetFWWPState(ctx, fwwpState); err != nil {
return errors.Wrapf(err, "failed to %s firmware write protect", enableStr)
}
} else {
// Enable software wp before hardware wp if enabling.
if enable {
if err := h.Servo.RunECCommand(ctx, "flashwp enable"); err != nil {
return errors.Wrap(err, "failed to enable flashwp")
}
}
if err := h.Servo.SetFWWPState(ctx, fwwpState); err != nil {
return errors.Wrapf(err, "failed to %s firmware write protect", enableStr)
}
// Disable software wp after hardware wp so its allowed.
if !enable {
if err := h.Servo.RunECCommand(ctx, "flashwp disable"); err != nil {
return errors.Wrap(err, "failed to disable flashwp")
}
}
}
return performModeAwareReboot(ctx, h)
}
func performModeAwareReboot(ctx context.Context, h *firmware.Helper) error {
// Create new mode switcher every time to prevent nil pointer errors.
ms, err := firmware.NewModeSwitcher(ctx, h)
if err != nil {
return errors.Wrap(err, "failed to create mode switcher")
}
testing.ContextLog(ctx, "Performing mode aware reboot")
return ms.ModeAwareReboot(ctx, firmware.ColdReset)
}
func checkCrossystem(ctx context.Context, h *firmware.Helper, expectedWpsw int) error {
r := reporters.New(h.DUT)
testing.ContextLog(ctx, "Check crossystem for write protect state param")
paramMap, err := r.Crossystem(ctx, reporters.CrossystemParamWpswCur)
if err != nil {
return errors.Wrapf(err, "failed to get crossystem %v value", reporters.CrossystemParamWpswCur)
}
currWpsw, err := strconv.Atoi(paramMap[reporters.CrossystemParamWpswCur])
if err != nil {
return errors.Wrap(err, "failed to convert crossystem wpsw value to integer value")
}
testing.ContextLogf(ctx, "Current write protect state: %v, Expected state: %v", currWpsw, expectedWpsw)
if currWpsw != expectedWpsw {
return errors.Errorf("expected WP state to %v, is actually %v", expectedWpsw, currWpsw)
}
return nil
}