Adds A Utility To Intentionally Corrupt DUT CBI Contents (go/cbi-auto-recovery-dd)

BUG=b:235000813
TEST=manual

Change-Id: Id9edc20fd19f1303bb5b3a20e161b50dbc20de0c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crostestutils/+/3928055
Reviewed-by: Andrew Luo <aluo@chromium.org>
Commit-Queue: Sean Carpenter <seancarpenter@google.com>
Reviewed-by: Sean Carpenter <seancarpenter@google.com>
Tested-by: Sean Carpenter <seancarpenter@google.com>
diff --git a/go/src/firmware/README.md b/go/src/firmware/README.md
index 9f48e34..033b73c 100644
--- a/go/src/firmware/README.md
+++ b/go/src/firmware/README.md
@@ -5,5 +5,6 @@
   referenced in the [firmware test manual](https://chromium.googlesource.com/chromiumos/docs/+/master/firmware_test_manual.md).
 * fw_e2e_coverage_summarizer: Used to generate firmware coverage reports (if demonstrating sufficient value, this utility will eventually be generalized to other, non-firmware test areas).
 * fw_lab_triage_helper: Used to surface information that is useful for isolating problems during triage of firmware lab test results.
+* corrupt_dut_cbi: Replaces the CBI contents on a DUT with garbage. Used in conjunction with Paris CBI Repair to test breaking and fixing CBI on our lab DUTs. See go/cbi-auto-recovery-dd for full background.
 
 Maintainers contact: cros-fw-te@google.com
diff --git a/go/src/firmware/cmd/corrupt_dut_cbi/corrupt_dut_cbi.go b/go/src/firmware/cmd/corrupt_dut_cbi/corrupt_dut_cbi.go
new file mode 100644
index 0000000..619c42d
--- /dev/null
+++ b/go/src/firmware/cmd/corrupt_dut_cbi/corrupt_dut_cbi.go
@@ -0,0 +1,228 @@
+// 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.
+
+// Does what it says on the tin. Overwrites the CBI contents on a DUT with
+// "0xff" values. To run, make sure you have ssh access setup for lab DUTs
+// go/chromeos-lab-duts-ssh and then `go run corrupt_dut_cbi.go <hostname>`.
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"os/exec"
+	"regexp"
+	"strings"
+
+	"errors"
+)
+
+// CBILocation stores the port and address needed to reference CBI contents in
+// EEPROM.
+type CBILocation struct {
+	port    string
+	address string
+}
+
+const (
+	cbiMagic           = "0x43 0x42 0x49" // Magic bytes used to indicate the contents of the CBI chip.
+	locateCBICommand   = "ectool locatechip 0 0"
+	transferCBICommand = "ectool i2cxfer"
+
+	cbiSize = 256 // How many bytes of memory are stored in CBI.
+
+	// How many bytes to read during our write operation. This is a quirk of the
+	// ectool i2cxfer API, and is always zero when writing.
+	numBytesToReadDuringWrite = "0"
+
+	// How many bytes can be read from and written to in a single ectool i2cxfer
+	// command. These should be treated as hard limits. Exceeding these limits
+	// can result in undefined writes to CBI, leaving the DUT in an obviously
+	// corrupted, but unpredictable state.
+	readIncrement  = 64
+	writeIncrement = 8
+)
+
+var readCBIRegex = regexp.MustCompile(`0x[[:xdigit:]]{1,2}|00`) // Match bytes printed in hex format (e.g. 00, 0x12, 0x3)
+var locateCBIRegex = regexp.MustCompile(`Port:\s(\d+).*Address:\s(0x\w+)`)
+var corruptCBILine = strings.Repeat("0xff ", writeIncrement)
+
+func main() {
+	if len(os.Args) != 2 {
+		fmt.Println("usage: corrupt_dut_cbi <hostname>")
+		return
+	}
+
+	_, stderr := executeRemoteCommand("")
+	if stderr != "" {
+		log.Panicf("unable to establish connection to the DUT: %s", stderr)
+	}
+
+	cbi, err := getCBILocation()
+	if err != nil {
+		log.Panicf("unable to determine if CBI is present on the DUT: %s", err)
+	}
+	fmt.Printf("CBI chip found at: Port %s, Address: %s\n\n", cbi.port, cbi.address)
+
+	cbiContents, err := cbi.readCBIContents()
+	if err != nil {
+		log.Panicf("unable to read initial CBI contents: %s", err)
+	}
+	if !strings.Contains(cbiContents, cbiMagic) {
+		log.Printf("CBI contents are already corrupt. Exiting early.")
+		return
+	}
+	printCBIContents(cbiContents)
+
+	err = cbi.corruptCBIContents()
+	if err != nil {
+		log.Panicf("unable to corrupt CBI contents: %s", err)
+	}
+
+	cbiContents, err = cbi.readCBIContents()
+	if err != nil {
+		log.Panicf("unable to read corrupted CBI contents: %s", err)
+	}
+
+	if strings.Contains(cbiContents, cbiMagic) {
+		printCBIContents(cbiContents)
+		log.Panic("Failed to corrupt CBI contents on DUT. CBI Magic is still present.")
+	}
+
+}
+
+// executeRemoteCommand ssh's into the DUT, executes the provided command and
+// returns the STDOUT and STDERR in string format.
+//
+// NOTE: executeRemoteCommand opens up a brand new SSH connection on every command
+// it runs. This is much slower than maintaining an open connection, but much simpler.
+// If speed of execution is ever a requirement, look here first to optimize.
+func executeRemoteCommand(command string) (string, string) {
+	log.Println(command)
+	hostName := os.Args[1]
+	cmd := exec.Command("ssh",
+		"-q",                                 // Mute ssh warnings and info messages
+		"-o UserKnownHostsFile=/dev/null",    // Avoid referencing any prior known hosts.
+		"-o StrictHostKeyChecking=no",        // Sometimes the host keys for DUTs in our lab change. That is okay and should be ignored.
+		"-o IdentityFile=~/.ssh/testing_rsa", // go/chromeos-lab-duts-ssh
+		hostName,
+		command,
+	)
+	var outbuf, errbuf strings.Builder
+	cmd.Stdout = &outbuf
+	cmd.Stderr = &errbuf
+	cmd.Run()
+
+	return outbuf.String(), errbuf.String()
+}
+
+// readCBIContents returns the CBI contents of the DUT as a hex string.
+func (cbi CBILocation) readCBIContents() (string, error) {
+	var hexContents []string
+	for _, readCommand := range cbi.generateReadCommands() {
+		stdout, stderr := executeRemoteCommand(readCommand)
+		if stderr != "" || stdout == "" {
+			return "", errors.New("unable to read CBI contents: " + stderr)
+		}
+
+		hexBytes, err := parseBytesFromCBIContents(stdout)
+		if err != nil {
+			return "", err
+		}
+
+		hexContents = append(hexContents, hexBytes...)
+	}
+	return strings.Join(hexContents, " "), nil
+}
+
+// corruptCBIContents generates and executes the write commands to corrupt the DUT
+func (cbi CBILocation) corruptCBIContents() error {
+	for _, corruptionCommand := range cbi.generateCorruptionCommand() {
+		stdout, stderr := executeRemoteCommand(corruptionCommand)
+		if stderr != "" || stdout == "" {
+			return errors.New("unable to write CBI contents: " + stderr)
+		}
+	}
+
+	return nil
+}
+
+// parseBytesFromCBIContents reads readIncrement number of bytes from the
+// raw output from a call to `ectool i2cxfer` and returns a slice of bytes
+// in hex format (the same format returned from `ectool i2cxfer`).
+// e.g.
+// cbiContents = "Read bytes: 0x43, 0x42, 0x49"
+// numBytesToRead = 2
+// hexBytes = ["0x43", "0x42"]
+func parseBytesFromCBIContents(cbiContents string) ([]string, error) {
+	hexBytes := readCBIRegex.FindAllString(cbiContents, readIncrement)
+	if len(hexBytes) != readIncrement {
+		return nil, fmt.Errorf("read the incorrect amount of bytes from CBI. Intended to read %d bytes but read %d bytes instead. CBI Contents found: %s", readIncrement, len(hexBytes), cbiContents)
+	}
+	return hexBytes, nil
+}
+
+// generateCorruptionCommand returns a list of sequential write commands that when
+// executed corrupt the contents of the dut <writeIncrement> bytes at a time.
+func (cbi *CBILocation) generateCorruptionCommand() []string {
+	var corruptionCommands []string
+	for offset := 0; offset < cbiSize; offset += writeIncrement {
+		corruptionCommands = append(corruptionCommands, fmt.Sprintf("%s %s %s %s %d %s",
+			transferCBICommand,
+			cbi.port,
+			cbi.address,
+			numBytesToReadDuringWrite,
+			offset,
+			corruptCBILine,
+		))
+	}
+	return corruptionCommands
+}
+
+// generateReadCommands returns a list of sequential read commands that when
+// executed read the contents of the dut <readIncrement> bytes at a time.
+func (cbi CBILocation) generateReadCommands() []string {
+	var readCommands []string
+	for offset := 0; offset < cbiSize; offset += readIncrement {
+		readCommands = append(readCommands, fmt.Sprintf("%s %s %s %d %d",
+			transferCBICommand,
+			cbi.port,
+			cbi.address,
+			readIncrement,
+			offset,
+		))
+	}
+	return readCommands
+}
+
+// getCBILocation uses the `ectool locatechip` utility to get the CBILocation
+// from the DUT. Will return an error if the DUT doesn't support CBI or if it
+// wasn't able to reach the DUT.
+func getCBILocation() (*CBILocation, error) {
+	stdout, stderr := executeRemoteCommand(locateCBICommand)
+	if stderr != "" {
+		return nil, errors.New("unable to determine if CBI is present on the DUT")
+	}
+
+	match := locateCBIRegex.FindStringSubmatch(stdout)
+	if match == nil {
+		return nil, errors.New("no CBI contents were found on the DUT")
+	}
+	return &CBILocation{
+		port:    match[1],
+		address: match[2],
+	}, nil
+
+}
+
+func printCBIContents(cbiContents string) {
+	fmt.Println("\n=======================================")
+	fmt.Println("CBI Contents")
+	fmt.Println("=======================================")
+	hexBytes := strings.Fields(cbiContents)
+	for offset := 0; offset < cbiSize; offset += writeIncrement {
+		fmt.Println(strings.Join(hexBytes[offset:offset+writeIncrement], " "))
+	}
+	fmt.Print("=======================================\n\n")
+}
diff --git a/go/src/firmware/go.mod b/go/src/firmware/go.mod
index 15ab94a..1429d95 100644
--- a/go/src/firmware/go.mod
+++ b/go/src/firmware/go.mod
@@ -1,5 +1,8 @@
 module go.chromium.org/fw-engprod-tools
+
 go 1.14
 
-require google.golang.org/api v0.60.0
-require golang.org/x/crypto v0.0.0-20220309165113-aa10faf2a1f8
+require (
+	golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8
+	golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
+)
diff --git a/go/src/firmware/go.sum b/go/src/firmware/go.sum
new file mode 100644
index 0000000..610c1a1
--- /dev/null
+++ b/go/src/firmware/go.sum
@@ -0,0 +1,12 @@
+golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8 h1:kACShD3qhmr/3rLmg1yXyt+N4HcwutKyPRB93s54TIU=
+golang.org/x/crypto v0.0.0-20220126234351-aa10faf2a1f8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=