blob: 619c42d8f36d60da446c0b12858435548a65a2da [file] [log] [blame]
// 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")
}