//go:build linux // tera-recovery-updater is the static PID 1 for authenticated offline updates. package main import ( "bufio" "encoding/json" "errors" "fmt" "io" "os" "strconv" "strings" "time" "golang.org/x/sys/unix" "tera-clone/services/tera-api/internal/update" "tera-clone/services/tera-api/internal/update/recoveryapply" "tera-clone/services/tera-api/internal/update/recoveryrequest" ) const ( updateRoot = "/userdata/update" trustDirectory = "/etc/tera/update-trust" product = "tera-core-x48-pro" hardwareProfile = "fet3568j-c" sectorBytes = 512 ) 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, "") // ttyS2 is a best-effort development trace only. The final product does not // expose COM5; durable status and journal files in userdata are authoritative. 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-updater] "+format+"\r\n", values...) } log("START authenticated offline recovery updater") cmdline, err := os.ReadFile("/proc/cmdline") if err != nil || !containsWord(string(cmdline), "tera.recovery=update") || !containsWord(string(cmdline), "rdinit=/init") { hold(log, "FAIL unexpected recovery kernel command line: %q (%v)", strings.TrimSpace(string(cmdline)), err) } for _, partition := range recoveryPartitionContract { if err := requirePartName(partition.UEventPath, partition.Name); err != nil { hold(log, "FAIL partition identity %s: %v", partition.Name, err) } } misc, err := os.OpenFile(miscDevice, os.O_RDWR, 0) if err != nil { hold(log, "FAIL open misc: %v", err) } message, err := recoveryrequest.ReadAt(misc) if err != nil { hold(log, "FAIL read authenticated recovery request: %v", err) } log("PASS recovery request operation=%s", message.OperationID) if err := unix.Mount(userdataDevice, "/userdata", "ext4", unix.MS_NODEV|unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NOATIME, userdataMountData); err != nil { hold(log, "FAIL mount userdata: %v", err) } bootBytes, err := blockBytes(bootSizePath) if err != nil { hold(log, "FAIL boot partition size: %v", err) } rootfsBytes, err := blockBytes(rootfsSizePath) if err != nil { hold(log, "FAIL rootfs partition size: %v", err) } packagePath := updateRoot + "/validated/" + message.OperationID + ".pkg" statusPath := updateRoot + "/journal/status.json" deviceVersion, err := readDeviceVersion(statusPath, message.OperationID) if err != nil { hold(log, "FAIL durable device-version binding: %v", err) } engine, err := recoveryapply.New(recoveryapply.Options{ OperationID: message.OperationID, PackagePath: packagePath, JournalPath: recoveryapply.JournalPath(updateRoot, message.OperationID), StatusPath: statusPath, TrustDirectory: trustDirectory, Product: product, Profile: hardwareProfile, CurrentVersion: deviceVersion, Targets: map[string]recoveryapply.Target{ "rootfs": {Path: rootfsDevice, MaxBytes: rootfsBytes}, "boot": {Path: bootDevice, MaxBytes: bootBytes}, }, ClearRecovery: func() error { return recoveryrequest.WriteAt(misc, recoveryrequest.Clear()) }, }) if err != nil { hold(log, "FAIL recovery engine setup: %v", err) } if err := engine.Run(); err != nil { hold(log, "FAIL recovery apply: %v", err) } log("PASS all selected payloads and readbacks verified; recovery request cleared") _ = misc.Close() unix.Sync() log("REBOOT normal boot") if err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART); err != nil { hold(log, "FAIL reboot normal system: %v", err) } for { time.Sleep(time.Hour) } } 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 blockBytes(path string) (int64, error) { data, err := os.ReadFile(path) if err != nil { return 0, err } sectors, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) if err != nil || sectors <= 0 || sectors > (1<<62)/sectorBytes { return 0, errors.New("invalid block-sector count") } return sectors * sectorBytes, nil } func readDeviceVersion(path, operationID string) (string, error) { file, err := os.Open(path) if err != nil { return "", err } defer file.Close() var status update.Status decoder := json.NewDecoder(io.LimitReader(file, 1024*1024)) if err := decoder.Decode(&status); err != nil { return "", err } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { return "", errors.New("status journal must contain exactly one JSON value") } if status.OperationID != operationID || strings.TrimSpace(status.DeviceVersion) == "" { return "", errors.New("status journal operation or device version does not match recovery request") } return status.DeviceVersion, nil } func hold(log func(string, ...any), format string, values ...any) { log(format, values...) for { time.Sleep(time.Hour) } }