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.
 
 
 
 
 
 

189 lines
5.7 KiB

//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)
}
}