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