Download
250 lines 6.5 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
package sandbox

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"syscall"
	"time"
)

// ApplyCommand is the hidden subcommand that makes a preview's changes
// inside the sandbox (see Apply): "blinker __apply", given its plan on
// stdin.
const ApplyCommand = "__apply"

// applyOp is a Change as the apply helper gets it.
type applyOp struct {
	Kind   ChangeKind
	Root   string // the layer's directory, as the sandbox sees it
	Rel    string // the path, relative to Root
	Mode   fs.FileMode
	Target string `json:",omitempty"`
	Opaque bool   `json:",omitempty"`
	Size   int64  `json:",omitempty"` // a Write's, whose contents follow the plan
	// Before is what the path must hold, or nil for one in a directory
	// that is new or replaces what was there, which the check of that
	// directory covers.
	Before *State `json:",omitempty"`
}

// Apply makes a preview's changes, in the sandbox, in the mode (and with
// the policy) the command ran in, so the kernel judges each write as it
// would have the command's. It applies nothing if any changed path is no
// longer as the preview found it: the user approved changes to what they
// saw. It does not discard the preview.
func (s *Sandbox) Apply(ctx context.Context, p *Preview) error {
	ops := make([]applyOp, len(p.Changes))
	for i, c := range p.Changes {
		ops[i] = applyOp{Kind: c.Kind, Root: c.root, Rel: c.rel, Mode: c.Mode, Target: c.Target, Opaque: c.Opaque, Size: c.Size}
		if !c.fresh {
			ops[i].Before = &p.Changes[i].Before
		}
	}
	plan, err := json.Marshal(ops)
	if err != nil {
		return err
	}
	pr, pw := io.Pipe()
	go func() {
		pw.CloseWithError(sendPlan(pw, plan, p.Changes))
	}()
	defer pr.Close() // ends sendPlan if the helper stops reading
	var stderr bytes.Buffer
	res, err := s.Run(ctx, Cmd{
		Mode: p.mode, Policy: p.policy, helper: ApplyCommand,
		Stdin: pr, Stdout: io.Discard, Stderr: &stderr, Timeout: 10 * time.Minute,
	})
	switch {
	case err != nil:
		return err
	case res.TimedOut:
		return errors.New("applying the changes timed out")
	case res.ExitCode != 0 && strings.TrimSpace(stderr.String()) == "":
		return fmt.Errorf("applying the changes failed, with exit code %d", res.ExitCode)
	case res.ExitCode != 0:
		return errors.New(strings.TrimSpace(stderr.String()))
	}
	return nil
}

// sendPlan writes the plan, a line, and then each Write's contents, of the
// size the plan gives.
func sendPlan(w io.Writer, plan []byte, changes []Change) error {
	if _, err := w.Write(append(plan, '\n')); err != nil {
		return err
	}
	for _, c := range changes {
		if c.Kind != Write {
			continue
		}
		f, err := os.Open(c.upper)
		if err != nil {
			return err
		}
		_, err = io.CopyN(w, f, c.Size)
		f.Close()
		if err != nil {
			return err
		}
	}
	return nil
}

// runApply is the apply helper: it reads the plan from stdin, checks every
// path is as the preview found it, and then makes the changes, in order.
func runApply(args []string) int {
	if len(args) != 0 {
		fmt.Fprintln(os.Stderr, "usage: blinker __apply < plan")
		return 2
	}
	if err := apply(bufio.NewReader(os.Stdin)); err != nil {
		fmt.Fprintln(os.Stderr, err)
		return 1
	}
	return 0
}

func apply(r *bufio.Reader) error {
	line, err := r.ReadBytes('\n')
	if err != nil {
		return fmt.Errorf("reading the plan: %w", err)
	}
	var ops []applyOp
	if err := json.Unmarshal(line, &ops); err != nil {
		return fmt.Errorf("reading the plan: %w", err)
	}
	// Each op's path stays inside its root: os.Root follows no symlink, nor
	// "..", out of it.
	roots := map[string]*os.Root{}
	defer func() {
		for _, root := range roots {
			root.Close()
		}
	}()
	for _, op := range ops {
		if roots[op.Root] != nil {
			continue
		}
		root, err := os.OpenRoot(op.Root)
		if err != nil {
			return err
		}
		roots[op.Root] = root
	}
	var changed []string
	for _, op := range ops {
		if op.Before == nil {
			continue
		}
		now, err := stateAt(roots[op.Root], op.Rel)
		if err != nil {
			return err
		}
		if now != *op.Before {
			changed = append(changed, filepath.Join(op.Root, op.Rel))
		}
	}
	if len(changed) > 0 {
		return fmt.Errorf("nothing was applied: changed since the preview: %s", strings.Join(changed, ", "))
	}
	// Directories get their modes last, deepest first, so one the command
	// closed (chmod 555) still takes what goes in it.
	var dirs []applyOp
	for _, op := range ops {
		if err := applyOne(roots[op.Root], op, r); err != nil {
			return fmt.Errorf("applying the changes stopped at %s, with those before it made: %w", filepath.Join(op.Root, op.Rel), err)
		}
		if op.Kind == Mkdir {
			dirs = append(dirs, op)
		}
	}
	for _, op := range slices.Backward(dirs) {
		if err := roots[op.Root].Chmod(op.Rel, op.Mode); err != nil {
			return fmt.Errorf("applying the changes stopped at the mode of %s, with the rest made: %w", filepath.Join(op.Root, op.Rel), err)
		}
	}
	return nil
}

// applyOne makes one change. Each replaces what is at its path, but for
// a Mkdir that is not Opaque, which keeps what the directory holds. A
// Mkdir's mode is left to the caller.
func applyOne(root *os.Root, op applyOp, contents io.Reader) error {
	name := op.Rel
	fi, err := root.Lstat(name)
	exists := err == nil
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return err
	}
	switch {
	case op.Kind == Mkdir && exists && fi.IsDir():
		if op.Opaque {
			return emptyDir(root, name)
		}
		return nil
	}
	if exists && name != "." {
		if err := root.RemoveAll(name); err != nil {
			return err
		}
	}
	if op.Kind != Remove {
		if err := root.MkdirAll(filepath.Dir(name), 0o755); err != nil {
			return err
		}
	}
	switch op.Kind {
	case Remove:
		return nil
	case Mkdir:
		return root.Mkdir(name, 0o700)
	case Symlink:
		return root.Symlink(op.Target, name)
	case Write:
		f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW, op.Mode)
		if err != nil {
			return err
		}
		n, err := io.CopyN(f, contents, op.Size)
		if err == nil {
			err = f.Chmod(op.Mode)
		}
		if closeErr := f.Close(); err == nil {
			err = closeErr
		}
		if err != nil {
			return fmt.Errorf("writing it (%d of %d bytes): %w", n, op.Size, err)
		}
		return nil
	}
	return fmt.Errorf("an unknown change, %d", op.Kind)
}

// emptyDir removes what a directory in root holds.
func emptyDir(root *os.Root, name string) error {
	dir, err := root.Open(name)
	if err != nil {
		return err
	}
	entries, err := dir.ReadDir(-1)
	dir.Close()
	if err != nil {
		return err
	}
	for _, e := range entries {
		if err := root.RemoveAll(filepath.Join(name, e.Name())); err != nil {
			return err
		}
	}
	return nil
}