| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package firmware |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/golang/protobuf/ptypes/empty" |
| "golang.org/x/mod/semver" |
| |
| "go.chromium.org/tast-tests/cros/common/servo" |
| "go.chromium.org/tast-tests/cros/common/testexec" |
| "go.chromium.org/tast-tests/cros/remote/firmware" |
| "go.chromium.org/tast-tests/cros/remote/firmware/fixture" |
| fwpb "go.chromium.org/tast-tests/cros/services/cros/firmware" |
| "go.chromium.org/tast/core/ctxutil" |
| "go.chromium.org/tast/core/dut" |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/ssh" |
| "go.chromium.org/tast/core/ssh/linuxssh" |
| "go.chromium.org/tast/core/testing" |
| "go.chromium.org/tast/core/testing/hwdep" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: BaseECUpdate, |
| LacrosStatus: testing.LacrosVariantUnneeded, |
| Desc: "Check that detachable base notification appears upon firmware update", |
| Contacts: []string{ |
| "chromeos-faft@google.com", |
| "cienet-firmware@cienet.corp-partner.google.com", |
| }, |
| BugComponent: "b:792402", // ChromeOS > Platform > Enablement > Firmware > FAFT |
| Attr: []string{"group:firmware", "firmware_ec", "firmware_detachable"}, |
| Requirements: []string{"sys-fw-0022-v02"}, |
| SoftwareDeps: []string{"chrome"}, |
| ServiceDeps: []string{"tast.cros.firmware.UtilsService"}, |
| HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.FormFactor(hwdep.Detachable), hwdep.Keyboard()), |
| Fixture: fixture.DevModeGBB, |
| Timeout: 15 * time.Minute, |
| }) |
| } |
| |
| // baseECInfo contains information about base ec. |
| type baseECInfo struct { |
| name string |
| version string |
| protectionFlag string |
| roProtected bool |
| } |
| |
| // modifiedFileDir contains paths defined as follows, |
| // onLocal: path on the local machine. |
| // onHost: path on DUT, where the modified base ec bin file will be copied to. |
| type modifiedFileDir struct { |
| onLocal string |
| onHost string |
| } |
| |
| // hammerRequiredVariables contains the required values for hammer command. |
| type hammerRequiredVariables struct { |
| pid string |
| vid string |
| usbPath string |
| } |
| |
| type baseStateSetter interface { |
| SetBaseState(ctx context.Context, state firmware.ECToolBaseState) error |
| } |
| |
| type gpioBaseStateSetter struct { |
| name string |
| ecTool *firmware.ECTool |
| } |
| |
| func (g *gpioBaseStateSetter) SetBaseState(ctx context.Context, state firmware.ECToolBaseState) error { |
| cmdList := []string{"gpioset", g.name} |
| |
| switch state { |
| case firmware.BaseAttach, firmware.BaseAuto: |
| cmdList = append(cmdList, "1") |
| case firmware.BaseDetach: |
| cmdList = append(cmdList, "0") |
| default: |
| return errors.Errorf("unsupported state: %s", state) |
| } |
| |
| return g.ecTool.Command(ctx, cmdList...).Run(testexec.DumpLogOnError) |
| } |
| |
| func BaseECUpdate(ctx context.Context, s *testing.State) { |
| h := s.FixtValue().(*fixture.Value).Helper |
| |
| if err := h.RequireServo(ctx); err != nil { |
| s.Fatal("Failed to init servo: ", err) |
| } |
| |
| if err := h.RequireConfig(ctx); err != nil { |
| s.Fatal("Failed to get config: ", err) |
| } |
| |
| if err := h.RequireRPCUtils(ctx); err != nil { |
| s.Fatal("Requiring RPC utils: ", err) |
| } |
| |
| s.Log("Logging in to Chrome") |
| if _, err := h.RPCUtils.NewChrome(ctx, &empty.Empty{}); err != nil { |
| s.Fatal("Failed to create a new instance of Chrome: ", err) |
| } |
| |
| dut := s.DUT() |
| ecTool := firmware.NewECTool(dut, firmware.ECToolNameMain) |
| utilServiceClient := fwpb.NewUtilsServiceClient(h.RPCClient.Conn) |
| |
| hammerConfigs, err := getHammerConfig(ctx, h, utilServiceClient) |
| if err != nil { |
| s.Fatal("Failed to get hammer config: ", err) |
| } |
| |
| tempDir, err := os.MkdirTemp("", "BaseECUpdate") |
| if err != nil { |
| s.Fatal("Failed to create a temp dir") |
| } |
| defer os.RemoveAll(tempDir) |
| |
| // fileDir creates paths to save the modified base ec bin file at respective locations. |
| fileDir := modifiedFileDir{ |
| onLocal: filepath.Join(tempDir, "modifiedBaseECLocal.bin"), |
| onHost: filepath.Join(tempDir, "modifiedBaseECHost.bin"), |
| } |
| |
| s.Log("Saving the base ec firmware version before flashing an old image") |
| originalBaseEC, err := getBaseECInfo(ctx, dut, hammerConfigs.pid) |
| if err != nil { |
| s.Fatal("Failed to check base ec's version: ", err) |
| } |
| |
| if err := modifyBaseEC(ctx, dut, originalBaseEC, &fileDir); err != nil { |
| s.Fatal("Failed to modify base-ec: ", err) |
| } |
| |
| cleanupCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, 3*time.Minute) |
| defer cancel() |
| |
| // In case the detachable base did not update successfully, warm reset DUT |
| // at the end of the test to ensure that the base ec would be restored. |
| requiredReboot := true |
| defer func(ctx context.Context, reboot *bool) { |
| if *reboot { |
| // One of the previous steps in flashing base ec was probably |
| // unsuccessful, which might leave base ec unresponsive. |
| s.Log("Rebooting DUT to recover base ec") |
| h.CloseRPCConnection(ctx) |
| if err := h.Servo.SetPowerState(ctx, servo.PowerStateWarmReset); err != nil { |
| s.Fatal("Failed to reboot DUT by servo: ", err) |
| } |
| s.Log("Waiting for DUT to power ON") |
| waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, 2*time.Minute) |
| defer cancelWaitConnect() |
| |
| if err := h.WaitConnect(waitConnectCtx); err != nil { |
| s.Fatal("Failed to reconnect to DUT: ", err) |
| } |
| } |
| }(cleanupCtx, &requiredReboot) |
| |
| s.Log("Flashing an old image to detachable-base ec") |
| if err := flashAnOldImgToDetachableBaseEC(ctx, dut, hammerConfigs, fileDir.onHost); err != nil { |
| // If flashing an edited image fails, check whether the version |
| // has changed. Sometimes, this failure might relate to the protection |
| // pipeline designed to guarantee a file's integrity, like so: |
| // Error message: libminijail[9206]: child process 9207 exited |
| // with status 14. |
| s.Log("Failed to flash base ec to an old version: ", err) |
| flashedBaseEC, err := getBaseECInfo(ctx, dut, hammerConfigs.pid) |
| if err != nil { |
| s.Fatal("Failed to get base ec info after flash: ", err) |
| } |
| |
| s.Logf("Base ec version: %q [Before] v.s. %q [After]", originalBaseEC.version, flashedBaseEC.version) |
| if baseECVersionUnchanged(originalBaseEC.version[len(originalBaseEC.name)+1:], flashedBaseEC.version[len(flashedBaseEC.name)+1:]) { |
| s.Fatalf("Found base ec version unchanged, got before: %q, and after: %q", originalBaseEC.version, flashedBaseEC.version) |
| } |
| } |
| |
| // Given that DUT's base ec is running an old firmware, |
| // detaching then re-attaching base would trigger an update |
| // notification window to pop up in a logged in session. |
| if err := triggerAndFindNotification(ctx, ecTool, utilServiceClient, dut, originalBaseEC.roProtected); err != nil { |
| s.Fatal("Failed to trigger and find notification window: ", err) |
| } |
| |
| if !originalBaseEC.roProtected { |
| s.Log("Power-cycling DUT with a warm reset") |
| h.CloseRPCConnection(ctx) |
| if err := h.Servo.SetPowerState(ctx, servo.PowerStateWarmReset); err != nil { |
| s.Fatal("Failed to reboot DUT by servo: ", err) |
| } |
| s.Log("Waiting for DUT to power ON") |
| waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, 2*time.Minute) |
| defer cancelWaitConnect() |
| |
| if err := h.WaitConnect(waitConnectCtx); err != nil { |
| s.Fatal("Failed to reconnect to DUT: ", err) |
| } |
| } |
| requiredReboot = false |
| |
| s.Log("Saving the current base ec firmware version") |
| newBaseEC, err := getBaseECInfo(ctx, dut, hammerConfigs.pid) |
| if err != nil { |
| s.Fatal("Failed to check base ec's version: ", err) |
| } |
| |
| s.Log("Verifying that base ec restored after a reboot") |
| if !baseECVersionUnchanged(originalBaseEC.version[len(originalBaseEC.name)+1:], newBaseEC.version[len(newBaseEC.name)+1:]) { |
| s.Fatal("Failed to update the base ec back to default version") |
| } |
| } |
| |
| func flashAnOldImgToDetachableBaseEC(ctx context.Context, dut *dut.DUT, hammerConfigs hammerRequiredVariables, dstImg string) error { |
| if err := dut.Conn().CommandContext( |
| ctx, |
| "/sbin/minijail0", "-e", "-N", "-p", "-l", "-u", |
| "hammerd", "-g", "hammerd", "-c", "0002", "/usr/bin/hammerd", |
| "--ec_image_path="+dstImg, |
| "--product_id="+hammerConfigs.pid, |
| "--vendor_id="+hammerConfigs.vid, |
| "--usb_path="+hammerConfigs.usbPath, |
| "--update_if=always", |
| ).Run(testexec.DumpLogOnError); err != nil { |
| return errors.Wrap(err, "unable to run the hammerd command") |
| } |
| return nil |
| } |
| |
| func baseECVersionUnchanged(old, new string) bool { |
| return semver.Compare(old, new) == 0 |
| } |
| |
| func getBaseECInfo(ctx context.Context, dut *dut.DUT, productIDDecimal string) (baseECInfo, error) { |
| var baseEC baseECInfo |
| productID, _ := strconv.Atoi(productIDDecimal) |
| hexProductID := strconv.FormatInt(int64(productID), 16) |
| deviceParams := fmt.Sprintf("18d1:%s", hexProductID) |
| |
| outputUsbUpdater := "" |
| // Poll on the usb_updater2 command, as the first few iterations |
| // might run into the 'can't find device' error. |
| if err := testing.Poll(ctx, func(ctx context.Context) error { |
| output, err := dut.Conn().CommandContext(ctx, "usb_updater2", "-d", deviceParams, "-f").Output(testexec.DumpLogOnError) |
| if err != nil { |
| return errors.Wrap(err, "failed to run usb_updater2 command in the dut") |
| } |
| outputUsbUpdater = string(output) |
| return nil |
| }, &testing.PollOptions{Interval: 1 * time.Second, Timeout: 10 * time.Second}); err != nil { |
| return baseEC, errors.Wrap(err, "failed to get the info from usb_updater2") |
| } |
| |
| baseECInfoMap := map[string]*regexp.Regexp{ |
| "name": regexp.MustCompile(`version:\s+(\w+)_v`), |
| "version": regexp.MustCompile(`version:\s+(\w+.\w.\w+-\w+)`), |
| "protection flags": regexp.MustCompile(`Flash protection status:\s+(\w+)`), |
| } |
| |
| for k, v := range baseECInfoMap { |
| match := v.FindStringSubmatch(outputUsbUpdater) |
| if len(match) < 2 { |
| return baseEC, errors.Errorf("did not match regex %q in %q", v, outputUsbUpdater) |
| } |
| usbUpdater2Info := strings.TrimSpace(match[1]) |
| |
| switch k { |
| case "name": |
| baseEC.name = usbUpdater2Info |
| case "version": |
| baseEC.version = usbUpdater2Info |
| case "protection flags": |
| baseEC.protectionFlag = usbUpdater2Info |
| } |
| } |
| |
| flagInDecimal, err := strconv.ParseInt(baseEC.protectionFlag, 16, 64) |
| if err != nil { |
| return baseEC, errors.Wrap(err, "failed to convert protection flags into hexadecimal") |
| } |
| flagInBinary := strconv.FormatInt(flagInDecimal, 2) |
| // If the second bit is equal to 1, RO is protected now. |
| if len(flagInBinary) > 1 && string(flagInBinary[len(flagInBinary)-2]) == "1" { |
| baseEC.roProtected = true |
| } |
| |
| return baseEC, nil |
| } |
| |
| func triggerAndFindNotification(ctx context.Context, ecTool *firmware.ECTool, utilSvcClient fwpb.UtilsServiceClient, dut *dut.DUT, roProtected bool) error { |
| hammerdLog := "/var/log/hammerd.log" |
| originalHammerdID, err := hammerdProcessID(ctx, hammerdLog, dut) |
| if err != nil { |
| return errors.Wrap(err, "failed to get hammerd process id") |
| } |
| |
| setter, err := getBaseStateSetter(ctx, ecTool) |
| if err != nil { |
| errors.Wrap(err, "failed to get base state setter") |
| } |
| // Detach then re-attach detachable's base to trigger update |
| // notification. |
| // We assume the base keyboard is always physically attached to the |
| // device during testing, and we use `TabletAuto` to revert forcing base |
| // state, so the state is back to attached. |
| for _, state := range []firmware.ECToolBaseState{firmware.BaseDetach, firmware.BaseAuto} { |
| if err := setter.SetBaseState(ctx, state); err != nil { |
| return errors.Wrap(err, "failed to switch the base state") |
| } |
| // GoBigSleepLint: Allow some delay to ensure base attached/detached by setting the gpio. |
| if err := testing.Sleep(ctx, 10*time.Second); err != nil { |
| return errors.Wrap(err, "failed to sleep for 10 seconds for the command to fully propagate to the DUT") |
| } |
| } |
| |
| newHammerdID, err := hammerdProcessID(ctx, hammerdLog, dut) |
| if err != nil { |
| return errors.Wrap(err, "failed to get hammerd process id") |
| } |
| testing.ContextLogf(ctx, "Hammerd process ids: %s [before re-attach], %s [after re-attach]", originalHammerdID, newHammerdID) |
| |
| testing.ContextLog(ctx, "Finding notification window") |
| if _, err := utilSvcClient.FindSingleNode(ctx, &fwpb.NodeElement{Name: "Your detachable keyboard needs a critical update"}); err != nil { |
| if roProtected && strings.Contains(err.Error(), context.DeadlineExceeded.Error()) { |
| // When RO locked, broken RW would get restored by hammerd silently. |
| testing.ContextLog(ctx, "Found RO locked, skip verifying pop-up window") |
| return nil |
| } |
| if originalHammerdID == newHammerdID { |
| return errors.Wrap(err, "hammerd did not restart following base power-cycle") |
| } |
| return errors.Wrap(err, "failed to find notification of detachable keyboard update") |
| |
| } |
| return nil |
| } |
| |
| // modifyBaseEC copies the /lib/firmware/base-ec.fw to local, |
| // modifies its version -1 and puts it back to /tmp/ folder in DUT. |
| func modifyBaseEC(ctx context.Context, dut *dut.DUT, boardInfo baseECInfo, fileDir *modifiedFileDir) error { |
| originalBaseECBinFile := fmt.Sprintf("/lib/firmware/%s.fw", boardInfo.name) |
| |
| testing.ContextLog(ctx, "Copying base-ec.fw from DUT to local") |
| if err := linuxssh.GetFile(ctx, dut.Conn(), originalBaseECBinFile, fileDir.onLocal, linuxssh.DereferenceSymlinks); err != nil { |
| return errors.Wrap(err, "failed to copy base-ec.fw to local") |
| } |
| |
| f, err := os.Open(fileDir.onLocal) |
| if err != nil { |
| return errors.Wrap(err, "failed to open base-ec.bin") |
| } |
| defer f.Close() |
| |
| stat, err := f.Stat() |
| if err != nil { |
| return errors.Wrap(err, "failed to read base-ec.bin") |
| } |
| |
| reader := bufio.NewReader(f) |
| buf := make([]byte, stat.Size()) |
| |
| for { |
| _, err := reader.Read(buf) |
| if err != nil { |
| if err != io.EOF { |
| testing.ContextLog(ctx, "Unexpected error: ", err) |
| } |
| break |
| } |
| |
| } |
| testing.ContextLog(ctx, "Current base-ec version: ", boardInfo.version) |
| testing.ContextLog(ctx, "Starting to modify base-ec.bin") |
| |
| baseECWithVersion := boardInfo.name + "_v" |
| indexRWBoard := len(buf) |
| count := bytes.Count(buf, []byte(baseECWithVersion)) |
| if count == 0 { |
| return errors.Wrapf(err, "did not find %s in the base-ec.bin", baseECWithVersion) |
| } |
| |
| for i := 0; i < count; i++ { |
| indexRWBoard = bytes.LastIndex(buf[:indexRWBoard], []byte(baseECWithVersion)) |
| indexVersionToModify := indexRWBoard |
| indexVersionToModify += len(baseECWithVersion) |
| // version -1 |
| buf[indexVersionToModify] = buf[indexVersionToModify] - 1 |
| } |
| testing.ContextLog(ctx, "Modified base-ec version: ", string(buf[indexRWBoard:indexRWBoard+len(boardInfo.version)])) |
| |
| // create a new bin file. |
| file, err := os.Create(fileDir.onLocal) |
| if err != nil { |
| return errors.New("failed to create a new bin file") |
| } |
| defer func() { |
| os.Remove(fileDir.onLocal) |
| file.Close() |
| }() |
| |
| if _, err := file.Write(buf); err != nil { |
| return errors.New("failed to write modified binary code into a new file") |
| } |
| |
| testing.ContextLog(ctx, "Copy the modified base-ec.bin back to DUT") |
| if _, err := linuxssh.PutFiles(ctx, dut.Conn(), map[string]string{fileDir.onLocal: fileDir.onHost}, linuxssh.DereferenceSymlinks); err != nil { |
| return errors.Wrap(err, "failed to copy files into DUT") |
| } |
| return nil |
| } |
| |
| func getBaseStateSetter(ctx context.Context, ecTool *firmware.ECTool) (baseStateSetter, error) { |
| // Included in baseGpioNames are a list of possible gpios available for |
| // controlling the base state. The first one found from the list would |
| // be used in setting base state attached/detached. |
| // If no GPIO matched, use ECTool as base state setter. |
| baseGpioNames := []firmware.GpioName{firmware.ENBASE, firmware.ENPP3300POGO, firmware.PP3300DXBASE, firmware.BASEPWREN} |
| foundNames, err := ecTool.FindGPIOs(ctx, baseGpioNames) |
| if err != nil { |
| return nil, errors.Wrap(err, "FindGPIOs failed") |
| } |
| |
| for _, name := range baseGpioNames { |
| if _, ok := foundNames[name]; ok { |
| return &gpioBaseStateSetter{name: string(name), ecTool: ecTool}, nil |
| } |
| } |
| return ecTool, nil |
| } |
| |
| func hammerdProcessID(ctx context.Context, hammerdLog string, dut *dut.DUT) (string, error) { |
| cmd := fmt.Sprintf("tail -1 %s | cut -d ' ' -f3 | grep -o '[[:digit:]]*'", hammerdLog) |
| processID, err := dut.Conn().CommandContext(ctx, "bash", "-c", cmd).Output(ssh.DumpLogOnError) |
| if err != nil { |
| return "", errors.Wrapf(err, "failed to run %s", cmd) |
| } |
| return strings.TrimSpace(string(processID)), nil |
| } |
| |
| func getHammerConfig(ctx context.Context, h *firmware.Helper, utilServiceClient fwpb.UtilsServiceClient) (hammerRequiredVariables, error) { |
| // hammerConfigsMap contains information about pid, vid, and usbPath values of a |
| // detachable base for different models. This information was derived from manual |
| // testing and the following hammer file: |
| // https://chromium.googlesource.com/chromiumos/platform/ec/+/HEAD/board/hammer/variants.h |
| hammerConfigsMap := map[string]hammerRequiredVariables{ |
| "coachz": {pid: "20556", vid: "6353", usbPath: "1-1.4"}, |
| "nocturne": {pid: "20528", vid: "6353", usbPath: "1-7"}, |
| "soraka": {pid: "20523", vid: "6353", usbPath: "1-2"}, |
| "krane": {pid: "20540", vid: "6353", usbPath: "1-1.1"}, |
| "kakadu": {pid: "20548", vid: "6353", usbPath: "1-1.1"}, |
| "katsu": {pid: "20560", vid: "6353", usbPath: "1-1.1"}, |
| "homestar": {pid: "20562", vid: "6353", usbPath: "1-1.1"}, |
| "wormdingler": {pid: "20567", vid: "6353", usbPath: "1-1.3"}, |
| "quackingstick": {pid: "20571", vid: "6353", usbPath: "1-1.1"}, |
| } |
| |
| var hammerConfigs hammerRequiredVariables |
| assignConfigs := func() error { |
| testing.ContextLog(ctx, "Attempting detachable base attributes from the hammer file") |
| modelName, err := h.Reporter.Model(ctx) |
| if err != nil { |
| return errors.Wrap(err, "failed to get the dut's model") |
| } |
| hammerConfigs.pid = hammerConfigsMap[modelName].pid |
| hammerConfigs.vid = hammerConfigsMap[modelName].vid |
| hammerConfigs.usbPath = hammerConfigsMap[modelName].usbPath |
| return nil |
| } |
| |
| crosCfgRes, err := utilServiceClient.GetDetachableBaseValue(ctx, &empty.Empty{}) |
| if err != nil { |
| testing.ContextLog(ctx, "Failed to get detachable-base attribute values: ", err) |
| if err := assignConfigs(); err != nil { |
| return hammerConfigs, errors.Wrap(err, "usnable to set attributes") |
| } |
| } else { |
| hammerConfigs.pid = crosCfgRes.ProductId |
| hammerConfigs.vid = crosCfgRes.VendorId |
| hammerConfigs.usbPath = crosCfgRes.UsbPath |
| } |
| return hammerConfigs, nil |
| } |