You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
5.4 KiB
168 lines
5.4 KiB
// Package recoveryrequest implements the Rockchip non-Android FIT recovery
|
|
// request stored in the misc partition's bootloader_message (BCB) area.
|
|
package recoveryrequest
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
)
|
|
|
|
const (
|
|
// Offset is the BCB offset selected by the deployed Rockchip U-Boot for a
|
|
// non-Android FIT image. It is intentionally different from Android's
|
|
// traditional 2 KiB offset.
|
|
Offset int64 = 16 * 1024
|
|
Size = 2048
|
|
|
|
commandOffset = 0
|
|
commandSize = 32
|
|
statusOffset = commandOffset + commandSize
|
|
statusSize = 32
|
|
recoveryOffset = statusOffset + statusSize
|
|
recoverySize = 768
|
|
stageOffset = recoveryOffset + recoverySize
|
|
stageSize = 32
|
|
|
|
Command = "boot-recovery"
|
|
)
|
|
|
|
var safeOperationID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
|
|
|
// Message is the target-owned subset of Android's bootloader_message. Unknown
|
|
// or unrelated recovery messages are rejected instead of being reinterpreted.
|
|
type Message struct {
|
|
OperationID string
|
|
}
|
|
|
|
func Encode(operationID string) ([]byte, error) {
|
|
if !safeOperationID.MatchString(operationID) {
|
|
return nil, errors.New("recovery operation id must be 1-64 safe characters")
|
|
}
|
|
encoded := make([]byte, Size)
|
|
copy(encoded[commandOffset:commandOffset+commandSize], Command)
|
|
recovery := fmt.Sprintf("recovery\n--tera-update\n--operation=%s\n", operationID)
|
|
if len(recovery) >= recoverySize {
|
|
return nil, errors.New("recovery request exceeds the BCB recovery field")
|
|
}
|
|
copy(encoded[recoveryOffset:recoveryOffset+recoverySize], recovery)
|
|
return encoded, nil
|
|
}
|
|
|
|
func Decode(encoded []byte) (Message, error) {
|
|
if len(encoded) != Size {
|
|
return Message{}, fmt.Errorf("recovery BCB must be exactly %d bytes", Size)
|
|
}
|
|
if field(encoded, commandOffset, commandSize) != Command {
|
|
return Message{}, errors.New("BCB does not contain a TERA recovery command")
|
|
}
|
|
recovery := field(encoded, recoveryOffset, recoverySize)
|
|
const prefix = "recovery\n--tera-update\n--operation="
|
|
if len(recovery) <= len(prefix)+1 || recovery[:len(prefix)] != prefix || recovery[len(recovery)-1] != '\n' {
|
|
return Message{}, errors.New("BCB recovery arguments do not match the TERA protocol")
|
|
}
|
|
operationID := recovery[len(prefix) : len(recovery)-1]
|
|
if !safeOperationID.MatchString(operationID) {
|
|
return Message{}, errors.New("BCB contains an invalid recovery operation id")
|
|
}
|
|
return Message{OperationID: operationID}, nil
|
|
}
|
|
|
|
func Clear() []byte { return make([]byte, Size) }
|
|
|
|
func IsClear(encoded []byte) bool {
|
|
return len(encoded) == Size && bytes.Equal(encoded, Clear())
|
|
}
|
|
|
|
func ReadRecordAt(reader io.ReaderAt) ([]byte, error) {
|
|
encoded := make([]byte, Size)
|
|
read, err := reader.ReadAt(encoded, Offset)
|
|
if err != nil || read != Size {
|
|
return nil, fmt.Errorf("read recovery BCB record: read %d of %d bytes: %w", read, Size, err)
|
|
}
|
|
return encoded, nil
|
|
}
|
|
|
|
// ReadAt reads and validates one BCB record without changing the surrounding
|
|
// misc partition bytes.
|
|
func ReadAt(reader io.ReaderAt) (Message, error) {
|
|
encoded, err := ReadRecordAt(reader)
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
return Decode(encoded)
|
|
}
|
|
|
|
// WriteRequestFile arms recovery only when the current record is exactly
|
|
// cleared. Production callers must keep allowRegular false so only a block
|
|
// device can be used; tests may opt into a bounded regular-file fixture.
|
|
func WriteRequestFile(path, operationID string, allowRegular bool) error {
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("inspect misc device: %w", err)
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || (!allowRegular && info.Mode()&os.ModeDevice == 0) ||
|
|
(allowRegular && !info.Mode().IsRegular() && info.Mode()&os.ModeDevice == 0) {
|
|
return errors.New("misc path is not an approved non-symlink block device")
|
|
}
|
|
file, err := os.OpenFile(path, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("open misc device: %w", err)
|
|
}
|
|
defer file.Close()
|
|
current, err := ReadRecordAt(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !IsClear(current) {
|
|
return errors.New("misc recovery BCB is not cleared")
|
|
}
|
|
encoded, err := Encode(operationID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return WriteAt(file, encoded)
|
|
}
|
|
|
|
// WriteAt writes exactly one BCB record and verifies the bytes by readback.
|
|
// Durability is the caller's responsibility when writer is not an *os.File.
|
|
func WriteAt(writer interface {
|
|
io.ReaderAt
|
|
io.WriterAt
|
|
}, encoded []byte) error {
|
|
if len(encoded) != Size {
|
|
return fmt.Errorf("recovery BCB must be exactly %d bytes", Size)
|
|
}
|
|
written, err := writer.WriteAt(encoded, Offset)
|
|
if err != nil || written != Size {
|
|
return fmt.Errorf("write recovery BCB: wrote %d of %d bytes: %w", written, Size, err)
|
|
}
|
|
if file, ok := writer.(*os.File); ok {
|
|
if err := file.Sync(); err != nil {
|
|
return fmt.Errorf("sync recovery BCB: %w", err)
|
|
}
|
|
}
|
|
readback := make([]byte, Size)
|
|
read, err := writer.ReadAt(readback, Offset)
|
|
if err != nil || read != Size {
|
|
return fmt.Errorf("read back recovery BCB: read %d of %d bytes: %w", read, Size, err)
|
|
}
|
|
if !bytes.Equal(encoded, readback) {
|
|
return errors.New("recovery BCB readback mismatch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func field(encoded []byte, offset, size int) string {
|
|
value := encoded[offset : offset+size]
|
|
if index := bytes.IndexByte(value, 0); index >= 0 {
|
|
value = value[:index]
|
|
}
|
|
return string(value)
|
|
}
|
|
|
|
// Keep these offsets compile-time visible and guard accidental struct drift.
|
|
var _ = [1]struct{}{}[Size-(stageOffset+stageSize+1184)]
|
|
|