| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "io" |
| 7 | "os" |
| 8 | "os/user" |
| 9 | "path/filepath" |
| 10 | "strconv" |
| 11 | ) |
| 12 | |
| 13 | // The floor gate: `carlos system floor-allows-prev <floor file> <verdict |
| 14 | // file>`, which the two system units (carlos-system-update.service and |
| 15 | // carlos-system-recover.service) run as root, from carlos.prev, before |
| 16 | // they let carlos.prev run a recovery against an armed floor (moves spec |
| 17 | // §11; see movesfloor.go for the files). It replaces a `cmp -s` in the |
| 18 | // units' shell, which reopened the agent's floor file after the shell had |
| 19 | // tested it: between the two the agent, which owns /data, could put a |
| 20 | // symlink there (root then compared a file of the agent's choosing, a |
| 21 | // one-bit oracle) or a FIFO (root blocked until the unit timed out). |
| 22 | // |
| 23 | // The exit code is the answer: 0 when the verdict names the floor byte for |
| 24 | // byte, 1 for everything else (a mismatch, a refused or unreadable file, |
| 25 | // a misuse). The shell treats them all alike, so every failure here is |
| 26 | // fail-closed: the current binary runs, and its own Go check decides. |
| 27 | // |
| 28 | // carlos.prev runs the gate, not carlos, because the units pick carlos.prev |
| 29 | // on the premise that carlos is the candidate that did not work, and a |
| 30 | // gate in carlos would fail with it. Running carlos.prev for the gate is no |
| 31 | // new trust: root already runs `carlos.prev version` when a floor is armed |
| 32 | // (rootJudgePrev). A carlos.prev too old to have the verb exits non-zero, |
| 33 | // which the units read as a mismatch, so the current binary runs. |
| 34 | |
| 35 | // floorGateMaxBytes bounds both files. Each holds one release line, |
| 36 | // "vMAJOR.MINOR.PATCH\n" (writeFloorFiles, judgePrevWith). |
| 37 | const floorGateMaxBytes = 256 |
| 38 | |
| 39 | var ( |
| 40 | errFloorGateMismatch = errors.New("the verdict on carlos.prev does not name the floor on this box") |
| 41 | errFloorGateUnsafe = errors.New("is not a regular file with one link, at most 256 bytes, owned by an expected account") |
| 42 | errFloorGateUsage = errors.New("floor-allows-prev takes two absolute paths: the floor file and the verdict file") |
| 43 | ) |
| 44 | |
| 45 | // floorGateUnsafe is the refusal of a file the gate will not read (MV-N317), |
| 46 | // said whole in one format so the copy gate can read it. It wraps |
| 47 | // errFloorGateUnsafe for errors.Is, and its text is the sentence alone. |
| 48 | func floorGateUnsafe(path string) error { |
| 49 | return &floorGateUnsafeError{ |
| 50 | msg: fmt.Sprintf("%s is not a regular file with one link, at most 256 bytes, owned by an expected account", path), |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | type floorGateUnsafeError struct{ msg string } |
| 55 | |
| 56 | func (e *floorGateUnsafeError) Error() string { return e.msg } |
| 57 | func (e *floorGateUnsafeError) Unwrap() error { return errFloorGateUnsafe } |
| 58 | |
| 59 | // RunSystemFloorAllowsPrev is the verb's entry point. The floor file may be |
| 60 | // owned by root (root's apply writes it) or by the service account (the |
| 61 | // agent's refresh writes it); the verdict only by root. |
| 62 | func RunSystemFloorAllowsPrev(args []string) { |
| 63 | os.Exit(floorGateMain(args, floorGateFloorOwners(), []uint32{0}, os.Stderr)) |
| 64 | } |
| 65 | |
| 66 | // floorGateMain checks args, runs the gate and says why on stderr when |
| 67 | // carlos.prev may not run. It returns the exit code. |
| 68 | func floorGateMain(args []string, floorOwners, verdictOwners []uint32, stderr io.Writer) int { |
| 69 | err := errFloorGateUsage |
| 70 | if len(args) == 2 && args[0] == "--root" { |
| 71 | err = rootFloorAllowsPrev(args[1]) |
| 72 | } |
| 73 | if len(args) == 2 && filepath.IsAbs(args[0]) && filepath.IsAbs(args[1]) { |
| 74 | err = floorAllowsPrev(args[0], args[1], floorOwners, verdictOwners) |
| 75 | } |
| 76 | if err != nil { |
| 77 | fmt.Fprintf(stderr, "moves protocol floor: carlos.prev may not run: %v\n", err) |
| 78 | return 1 |
| 79 | } |
| 80 | return 0 |
| 81 | } |
| 82 | |
| 83 | // floorGateFloorOwners is root and, when the box has one, the service |
| 84 | // account. A box where the account cannot be looked up accepts root alone. |
| 85 | func floorGateFloorOwners() []uint32 { |
| 86 | owners := []uint32{0} |
| 87 | if u, err := user.Lookup(serviceUser); err == nil { |
| 88 | if uid, err := strconv.ParseUint(u.Uid, 10, 32); err == nil { |
| 89 | owners = append(owners, uint32(uid)) |
| 90 | } |
| 91 | } |
| 92 | return owners |
| 93 | } |
| 94 | |