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.
128 lines
4.1 KiB
128 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)
|
|
}
|
|
}
|
|
|