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.

129 lines
4.1 KiB

//go:build linux
// tera-recovery-probe is a deliberately read-only PID 1 used to prove the
// recovery boot contract before any partition-writing updater is introduced.
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"time"
"golang.org/x/sys/unix"
"tera-clone/services/tera-api/internal/update/recoveryrequest"
)
const (
miscDevice = "/dev/mmcblk0p2"
userdataDevice = "/dev/mmcblk0p8"
journalPath = "/userdata/update/journal/status.json"
)
func main() {
mustMount("devtmpfs", "/dev", "devtmpfs", unix.MS_NOSUID, "mode=0755")
mustMount("proc", "/proc", "proc", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")
mustMount("sysfs", "/sys", "sysfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV, "")
console, err := os.OpenFile("/dev/ttyS2", os.O_WRONLY, 0)
if err != nil {
console = os.Stdout
}
log := func(format string, values ...any) {
fmt.Fprintf(console, "[tera-recovery-probe] "+format+"\r\n", values...)
}
log("START read-only probe; no partition payload writes are implemented")
cmdline, err := os.ReadFile("/proc/cmdline")
if err != nil || !containsWord(string(cmdline), "tera.recovery=probe") || !containsWord(string(cmdline), "rdinit=/init") {
hold(log, "FAIL unexpected recovery kernel command line: %q (%v)", strings.TrimSpace(string(cmdline)), err)
return
}
log("PASS recovery kernel command line: %s", strings.TrimSpace(string(cmdline)))
if err := requirePartName("/sys/class/block/mmcblk0p2/uevent", "misc"); err != nil {
hold(log, "FAIL misc identity: %v", err)
return
}
if err := requirePartName("/sys/class/block/mmcblk0p8/uevent", "userdata"); err != nil {
hold(log, "FAIL userdata identity: %v", err)
return
}
misc, err := os.Open(miscDevice)
if err != nil {
hold(log, "FAIL open misc read-only: %v", err)
return
}
message, readErr := recoveryrequest.ReadAt(misc)
closeErr := misc.Close()
if readErr != nil || closeErr != nil {
hold(log, "FAIL recovery request: read=%v close=%v", readErr, closeErr)
return
}
log("PASS recovery request operation=%s", message.OperationID)
if err := unix.Mount(userdataDevice, "/userdata", "ext4", unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NOATIME, probeUserdataMountData); err != nil {
hold(log, "FAIL mount userdata read-only: %v", err)
return
}
journal, err := readJournalOperation(journalPath)
if err != nil || journal.OperationID != message.OperationID || journal.State != "validated" {
hold(log, "FAIL journal binding: request=%q journal=%q state=%q error=%v", message.OperationID, journal.OperationID, journal.State, err)
return
}
packagePath := "/userdata/update/validated/" + message.OperationID + ".pkg"
packageInfo, err := os.Lstat(packagePath)
if err != nil || !packageInfo.Mode().IsRegular() || packageInfo.Mode()&os.ModeSymlink != 0 || packageInfo.Size() <= 0 {
hold(log, "FAIL validated package identity: path=%s error=%v", packagePath, err)
return
}
log("PASS journal binding operation=%s state=%s package_bytes=%d", journal.OperationID, journal.State, packageInfo.Size())
log("READY boot selection, ttyS2, misc BCB, GPT identity, and read-only userdata journal are proven")
hold(log, "HOLD request intentionally remains set; use the service procedure to restore normal boot")
}
func mustMount(source, target, filesystem string, flags uintptr, data string) {
if err := unix.Mount(source, target, filesystem, flags, data); err != nil {
fmt.Fprintf(os.Stderr, "mount %s: %v\n", target, err)
for {
time.Sleep(time.Hour)
}
}
}
func containsWord(value, wanted string) bool {
for _, word := range strings.Fields(value) {
if word == wanted {
return true
}
}
return false
}
func requirePartName(path, wanted string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(io.LimitReader(file, 4096))
for scanner.Scan() {
if scanner.Text() == "PARTNAME="+wanted {
return nil
}
}
if err := scanner.Err(); err != nil {
return err
}
return fmt.Errorf("expected PARTNAME=%s", wanted)
}
func hold(log func(string, ...any), format string, values ...any) {
log(format, values...)
for {
time.Sleep(time.Hour)
}
}