1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//go:build linux

package builder

// 1:1 port of lix/libexec/launch-builder.cc main().
//
// This runs in the re-exec'd "__builder" child.  The parent has already:
//   - created the namespaces (user/mount/pid/net/...) via clone flags,
//   - written the uid/gid maps,
//   - connected a setup socket to this process's stdin (fd 0).
//
// The protocol over fd 0 mirrors lix: the parent writes the gob-encoded
// BuildRequest, then this child either closes fd 0 (success — handled by the
// final execve replacing the process) or writes a gob-encoded launcherError
// back on fd 0 before exiting non-zero.

import (
	"encoding/gob"
	"fmt"
	"os"

	"golang.org/x/sys/unix"
)

// launcherError is the failure response sent back to the parent over fd 0,
// mirroring lix's SetupResponse::fatalError.
type launcherError struct {
	Message string
}

// IsBuilderSubcommand reports whether argv indicates the re-exec'd launcher.
// The parent re-execs the gonix binary with this as the first argument.
const BuilderSubcommand = "__builder"

// MaybeLauncherMain checks whether the current process was re-exec'd as the
// sandbox launcher and, if so, calls LauncherMain (which never returns).
// Intended to be called from TestMain in test binaries.
func MaybeLauncherMain() {
	if len(os.Args) >= 2 && os.Args[1] == BuilderSubcommand {
		LauncherMain()
	}
}

// LauncherMain is the entry point for the "__builder" subcommand.  It never
// returns on success (it execve's the builder); on failure it reports the error
// to the parent and exits 1.
func LauncherMain() {
	if err := launcherRun(); err != nil {
		// Try to send the structured error back to the parent over fd 0.
		// If that fails (e.g. fd already past handshake), fall back to stderr.
		if encErr := sendFatalError(err); encErr != nil {
			fmt.Fprint(os.Stderr, err.Error())
		}
		os.Exit(1)
	}
	// launcherRun execve's on success and never returns; reaching here is a bug.
	fmt.Fprintln(os.Stderr, "builder: launcherRun returned without exec")
	os.Exit(1)
}

func launcherRun() error {
	// Read the build request from fd 0 (the setup socket).
	setup := os.NewFile(0, "setup")
	dec := gob.NewDecoder(setup)
	var req BuildRequest
	if err := dec.Decode(&req); err != nil {
		return fmt.Errorf("reading build request: %w", err)
	}

	// Unmask all signals (the parent may have masked some).
	var empty unix.Sigset_t
	if err := unix.PthreadSigmask(unix.SIG_SETMASK, &empty, nil); err != nil {
		return fmt.Errorf("failed to unmask signals: %w", err)
	}

	// Put the child in a new session (no controlling terminal).
	if _, err := unix.Setsid(); err != nil {
		return fmt.Errorf("creating a new session: %w", err)
	}

	// Dup stderr to stdout (build output goes to the log).
	if err := unix.Dup2(int(unix.Stderr), int(unix.Stdout)); err != nil {
		return fmt.Errorf("cannot dup stderr into stdout: %w", err)
	}

	setUser, err := prepareChildSetup(&req)
	if err != nil {
		return err
	}

	if err := unix.Chdir(req.WorkingDir); err != nil {
		return fmt.Errorf("changing into %s: %w", req.WorkingDir, err)
	}

	// Disable core dumps by default.
	limit := unix.Rlimit{Cur: 0, Max: ^uint64(0)}
	if req.EnableCoreDumps {
		limit.Cur = ^uint64(0)
	}
	unix.Setrlimit(unix.RLIMIT_CORE, &limit)

	// If running in build-users mode, switch to the allocated build user and
	// drop all root privileges.
	if setUser && req.Credentials != nil {
		creds := req.Credentials
		gids := make([]int, 0, len(creds.SupplementaryGroups))
		for _, g := range creds.SupplementaryGroups {
			gids = append(gids, int(g))
		}
		if err := unix.Setgroups(gids); err != nil {
			return fmt.Errorf("cannot set supplementary groups of build user: %w", err)
		}
		if err := unix.Setgid(int(creds.Gid)); err != nil {
			return fmt.Errorf("setgid failed: %w", err)
		}
		if err := unix.Setuid(int(creds.Uid)); err != nil {
			return fmt.Errorf("setuid failed: %w", err)
		}
	}

	if err := finishChildSetup(&req); err != nil {
		return err
	}

	// Close all FDs except std{in,out,err}.
	closeExtraFDs()

	// Reroute stdin from /dev/null.  Closing/overwriting fd 0 also signals
	// successful setup to the parent (it sees EOF on the setup socket).
	devNull, err := unix.Open("/dev/null", unix.O_RDWR|unix.O_CLOEXEC, 0)
	if err != nil {
		return fmt.Errorf("cannot open /dev/null: %w", err)
	}
	if err := unix.Dup2(devNull, int(unix.Stdin)); err != nil {
		return fmt.Errorf("cannot dup null device into stdin: %w", err)
	}

	// Past this point any error must go to stderr, not the (now closed) setup fd.
	pastHandshake = true

	return execBuilder(&req)
}

// pastHandshake tracks whether we've closed the setup fd; after that, errors go
// to stderr rather than being encoded back over fd 0.
var pastHandshake bool

func sendFatalError(err error) error {
	if pastHandshake {
		return fmt.Errorf("past handshake")
	}
	setup := os.NewFile(0, "setup")
	enc := gob.NewEncoder(setup)
	return enc.Encode(launcherError{Message: err.Error()})
}

// closeExtraFDs closes every open fd above stderr, mirroring closeExtraFDs in
// launch-builder.cc.  We use the portable /proc/self/fd iteration (the C++ code
// prefers close_range but falls back to exactly this).
func closeExtraFDs() {
	const maxKeptFD = 2 // stderr
	entries, err := os.ReadDir("/proc/self/fd")
	if err == nil {
		for _, e := range entries {
			var fd int
			if _, err := fmt.Sscanf(e.Name(), "%d", &fd); err != nil {
				continue
			}
			if fd > maxKeptFD {
				unix.Close(fd)
			}
		}
		return
	}
	// Fallback: close up to the soft NOFILE limit.
	var rl unix.Rlimit
	maxFD := 1024
	if unix.Getrlimit(unix.RLIMIT_NOFILE, &rl) == nil {
		maxFD = int(rl.Cur)
	}
	for fd := maxKeptFD + 1; fd < maxFD; fd++ {
		unix.Close(fd)
	}
}