package host import ( "errors" "fmt" "io" "os" "os/user" "path/filepath" "strconv" ) // The floor gate: `carlos system floor-allows-prev `, which the two system units (carlos-system-update.service and // carlos-system-recover.service) run as root, from carlos.prev, before // they let carlos.prev run a recovery against an armed floor (moves spec // ยง11; see movesfloor.go for the files). It replaces a `cmp -s` in the // units' shell, which reopened the agent's floor file after the shell had // tested it: between the two the agent, which owns /data, could put a // symlink there (root then compared a file of the agent's choosing, a // one-bit oracle) or a FIFO (root blocked until the unit timed out). // // The exit code is the answer: 0 when the verdict names the floor byte for // byte, 1 for everything else (a mismatch, a refused or unreadable file, // a misuse). The shell treats them all alike, so every failure here is // fail-closed: the current binary runs, and its own Go check decides. // // carlos.prev runs the gate, not carlos, because the units pick carlos.prev // on the premise that carlos is the candidate that did not work, and a // gate in carlos would fail with it. Running carlos.prev for the gate is no // new trust: root already runs `carlos.prev version` when a floor is armed // (rootJudgePrev). A carlos.prev too old to have the verb exits non-zero, // which the units read as a mismatch, so the current binary runs. // floorGateMaxBytes bounds both files. Each holds one release line, // "vMAJOR.MINOR.PATCH\n" (writeFloorFiles, judgePrevWith). const floorGateMaxBytes = 256 var ( errFloorGateMismatch = errors.New("the verdict on carlos.prev does not name the floor on this box") errFloorGateUnsafe = errors.New("is not a regular file with one link, at most 256 bytes, owned by an expected account") errFloorGateUsage = errors.New("floor-allows-prev takes two absolute paths: the floor file and the verdict file") ) // floorGateUnsafe is the refusal of a file the gate will not read (MV-N317), // said whole in one format so the copy gate can read it. It wraps // errFloorGateUnsafe for errors.Is, and its text is the sentence alone. func floorGateUnsafe(path string) error { return &floorGateUnsafeError{ msg: fmt.Sprintf("%s is not a regular file with one link, at most 256 bytes, owned by an expected account", path), } } type floorGateUnsafeError struct{ msg string } func (e *floorGateUnsafeError) Error() string { return e.msg } func (e *floorGateUnsafeError) Unwrap() error { return errFloorGateUnsafe } // RunSystemFloorAllowsPrev is the verb's entry point. The floor file may be // owned by root (root's apply writes it) or by the service account (the // agent's refresh writes it); the verdict only by root. func RunSystemFloorAllowsPrev(args []string) { os.Exit(floorGateMain(args, floorGateFloorOwners(), []uint32{0}, os.Stderr)) } // floorGateMain checks args, runs the gate and says why on stderr when // carlos.prev may not run. It returns the exit code. func floorGateMain(args []string, floorOwners, verdictOwners []uint32, stderr io.Writer) int { err := errFloorGateUsage if len(args) == 2 && args[0] == "--root" { err = rootFloorAllowsPrev(args[1]) } if len(args) == 2 && filepath.IsAbs(args[0]) && filepath.IsAbs(args[1]) { err = floorAllowsPrev(args[0], args[1], floorOwners, verdictOwners) } if err != nil { fmt.Fprintf(stderr, "moves protocol floor: carlos.prev may not run: %v\n", err) return 1 } return 0 } // floorGateFloorOwners is root and, when the box has one, the service // account. A box where the account cannot be looked up accepts root alone. func floorGateFloorOwners() []uint32 { owners := []uint32{0} if u, err := user.Lookup(serviceUser); err == nil { if uid, err := strconv.ParseUint(u.Uid, 10, 32); err == nil { owners = append(owners, uint32(uid)) } } return owners }