Skip to content

Commit 02a4ed2

Browse files
authored
Add the sessionview launcher for per-Session views (#339)
The daemon re-executes itself into a launcher cloned into new mount, PID and network namespaces. The launcher mounts the world over /dev/fuse, hands the connection to the daemon's world server over SCM_RIGHTS, binds the private directories, overlays and shim onto symlink-free targets, adds /proc and a minimal /dev, moves the world onto /, drops privileges, installs a seccomp filter that denies user namespaces, setns and clone3, and runs the Harness as PID 1's only child. Mount flags alone decide what can be executed.
1 parent 91e5ca1 commit 02a4ed2

15 files changed

Lines changed: 2308 additions & 0 deletions

File tree

‎apps/daemon/cmd/oac-daemon/main.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"os"
1010

1111
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/cli"
12+
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/sessionview"
1213
)
1314

1415
func main() {
16+
sessionview.Init()
1517
if err := cli.Execute(os.Args[1:]); err != nil {
1618
fmt.Fprintf(os.Stderr, "oac-daemon: %v\n", err)
1719
os.Exit(1)
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
//go:build linux
2+
3+
package sessionview
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"strings"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
// builder mounts the local pieces onto the world. Every target is resolved beneath its parent mount without following symlinks, so the sandbox cannot redirect a mount.
14+
type builder struct {
15+
root int // the world root, an O_PATH fd
16+
fds []int
17+
}
18+
19+
// devNodes are bound from the host's /dev into the view's /dev.
20+
var devNodes = []string{"null", "zero", "full", "random", "urandom", "tty"}
21+
22+
var devLinks = [][2]string{
23+
{"ptmx", "pts/ptmx"},
24+
{"fd", "/proc/self/fd"},
25+
{"stdin", "/proc/self/fd/0"},
26+
{"stdout", "/proc/self/fd/1"},
27+
{"stderr", "/proc/self/fd/2"},
28+
}
29+
30+
const (
31+
attrNoSuid = unix.MOUNT_ATTR_NOSUID
32+
attrNoDev = unix.MOUNT_ATTR_NODEV
33+
attrNoExec = unix.MOUNT_ATTR_NOEXEC
34+
)
35+
36+
func (b *builder) build(spec *launchSpec) error {
37+
root, err := unix.Open(spec.Staging, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
38+
if err != nil {
39+
return &Error{Kind: ErrLauncher, Op: "open", Path: spec.Staging, Err: err}
40+
}
41+
b.root = b.keep(root)
42+
for _, d := range spec.Private {
43+
src, err := b.source(d.HostDir)
44+
if err != nil {
45+
return err
46+
}
47+
if err := b.bind(src, b.root, privateRoot+"/"+d.Name, bindAttr(d.Writable, d.Exec, false)); err != nil {
48+
return err
49+
}
50+
}
51+
shim := -1
52+
if len(spec.Shim.Names) > 0 || len(spec.Shim.Paths) > 0 {
53+
if shim, err = b.source(spec.Shim.Binary); err != nil {
54+
return err
55+
}
56+
}
57+
if err := b.shimDir(spec.Shim.Names, shim); err != nil {
58+
return err
59+
}
60+
for _, o := range spec.Overlays {
61+
src, err := b.source(o.Source)
62+
if err != nil {
63+
return err
64+
}
65+
if err := b.bind(src, b.root, o.Path, bindAttr(false, o.Exec, false)); err != nil {
66+
return err
67+
}
68+
}
69+
for _, p := range spec.Shim.Paths {
70+
if err := b.bind(shim, b.root, p, bindAttr(false, true, false)); err != nil {
71+
return err
72+
}
73+
}
74+
proc, err := newFS("proc", nil, attrNoSuid|attrNoDev|attrNoExec)
75+
if err != nil {
76+
return err
77+
}
78+
defer unix.Close(proc)
79+
if err := b.attach(proc, b.root, "/proc", true); err != nil {
80+
return err
81+
}
82+
return b.dev()
83+
}
84+
85+
// shimDir presents the shim at /.oac/bin/<name> on a read-only tmpfs.
86+
func (b *builder) shimDir(names []string, shim int) error {
87+
dir := privateRoot + "/" + shimName
88+
mnt, err := newFS("tmpfs", [][2]string{{"mode", "0755"}, {"size", "64k"}}, attrNoSuid|attrNoDev|attrNoExec)
89+
if err != nil {
90+
return err
91+
}
92+
defer unix.Close(mnt)
93+
if err := b.attach(mnt, b.root, dir, true); err != nil {
94+
return err
95+
}
96+
for _, n := range names {
97+
if err := createFile(mnt, n, dir+"/"+n); err != nil {
98+
return err
99+
}
100+
if err := b.bindAt(shim, mnt, n, dir+"/"+n, bindAttr(false, true, false)); err != nil {
101+
return err
102+
}
103+
}
104+
return readOnly(mnt, dir)
105+
}
106+
107+
// dev builds a minimal read-only /dev with host device nodes, a new devpts instance and a noexec /dev/shm.
108+
func (b *builder) dev() error {
109+
mnt, err := newFS("tmpfs", [][2]string{{"mode", "0755"}, {"size", "64k"}}, attrNoSuid|attrNoDev|attrNoExec)
110+
if err != nil {
111+
return err
112+
}
113+
defer unix.Close(mnt)
114+
if err := b.attach(mnt, b.root, "/dev", true); err != nil {
115+
return err
116+
}
117+
for _, n := range devNodes {
118+
src, err := b.source("/dev/" + n)
119+
if err != nil {
120+
return err
121+
}
122+
if err := createFile(mnt, n, "/dev/"+n); err != nil {
123+
return err
124+
}
125+
if err := b.bindAt(src, mnt, n, "/dev/"+n, bindAttr(true, false, true)); err != nil {
126+
return err
127+
}
128+
}
129+
subs := []struct {
130+
name, fstype string
131+
opts [][2]string
132+
attr int
133+
}{
134+
{"pts", "devpts", [][2]string{{"ptmxmode", "0666"}, {"mode", "0620"}}, attrNoSuid | attrNoExec},
135+
{"shm", "tmpfs", [][2]string{{"mode", "1777"}}, attrNoSuid | attrNoDev | attrNoExec},
136+
}
137+
for _, s := range subs {
138+
if err := unix.Mkdirat(mnt, s.name, 0o755); err != nil {
139+
return &Error{Kind: ErrLauncher, Op: "mkdir", Path: "/dev/" + s.name, Err: err}
140+
}
141+
fs, err := newFS(s.fstype, s.opts, s.attr)
142+
if err != nil {
143+
return err
144+
}
145+
err = b.attachAt(fs, mnt, s.name, "/dev/"+s.name, true)
146+
unix.Close(fs)
147+
if err != nil {
148+
return err
149+
}
150+
}
151+
for _, l := range devLinks {
152+
if err := unix.Symlinkat(l[1], mnt, l[0]); err != nil {
153+
return &Error{Kind: ErrLauncher, Op: "symlink", Path: "/dev/" + l[0], Err: err}
154+
}
155+
}
156+
return readOnly(mnt, "/dev")
157+
}
158+
159+
// source opens a trusted host path.
160+
func (b *builder) source(path string) (int, error) {
161+
fd, err := unix.Open(path, unix.O_PATH|unix.O_CLOEXEC, 0)
162+
if err != nil {
163+
return -1, &Error{Kind: ErrLauncher, Op: "open", Path: path, Err: err}
164+
}
165+
return b.keep(fd), nil
166+
}
167+
168+
func (b *builder) keep(fd int) int {
169+
b.fds = append(b.fds, fd)
170+
return fd
171+
}
172+
173+
func (b *builder) close() {
174+
for _, fd := range b.fds {
175+
unix.Close(fd)
176+
}
177+
b.fds, b.root = nil, -1
178+
}
179+
180+
// bind mounts a clone of src at the absolute view path under the world root.
181+
func (b *builder) bind(src, root int, view string, attr *unix.MountAttr) error {
182+
return b.bindAt(src, root, strings.TrimPrefix(view, "/"), view, attr)
183+
}
184+
185+
func (b *builder) bindAt(src, dirfd int, rel, view string, attr *unix.MountAttr) error {
186+
var st unix.Stat_t
187+
if err := unix.Fstat(src, &st); err != nil {
188+
return &Error{Kind: ErrLauncher, Op: "stat source", Path: view, Err: err}
189+
}
190+
mnt, err := unix.OpenTree(src, "", unix.OPEN_TREE_CLONE|unix.OPEN_TREE_CLOEXEC|unix.AT_EMPTY_PATH)
191+
if err != nil {
192+
return mountError("open_tree", view, err)
193+
}
194+
defer unix.Close(mnt)
195+
if err := unix.MountSetattr(mnt, "", unix.AT_EMPTY_PATH, attr); err != nil {
196+
return mountError("mount_setattr", view, err)
197+
}
198+
return b.attachAt(mnt, dirfd, rel, view, st.Mode&unix.S_IFMT == unix.S_IFDIR)
199+
}
200+
201+
func (b *builder) attach(mnt, root int, view string, dir bool) error {
202+
return b.attachAt(mnt, root, strings.TrimPrefix(view, "/"), view, dir)
203+
}
204+
205+
// attachAt moves a detached mount onto rel beneath dirfd, which must be a directory when dir is set and a regular file otherwise.
206+
func (b *builder) attachAt(mnt, dirfd int, rel, view string, dir bool) error {
207+
target, err := resolve(dirfd, rel)
208+
if err != nil {
209+
return &Error{Kind: ErrMountTarget, Op: "resolve", Path: view, Err: err}
210+
}
211+
defer unix.Close(target)
212+
var st unix.Stat_t
213+
if err := unix.Fstat(target, &st); err != nil {
214+
return &Error{Kind: ErrMountTarget, Op: "stat", Path: view, Err: err}
215+
}
216+
switch kind := st.Mode & unix.S_IFMT; {
217+
case dir && kind != unix.S_IFDIR:
218+
return &Error{Kind: ErrMountTarget, Op: "check", Path: view, Err: unix.ENOTDIR}
219+
case !dir && kind != unix.S_IFREG:
220+
return &Error{Kind: ErrMountTarget, Op: "check", Path: view, Err: fmt.Errorf("not a regular file")}
221+
}
222+
if err := unix.MoveMount(mnt, "", target, "", unix.MOVE_MOUNT_F_EMPTY_PATH|unix.MOVE_MOUNT_T_EMPTY_PATH); err != nil {
223+
return mountError("move_mount", view, err)
224+
}
225+
return nil
226+
}
227+
228+
// resolve opens rel beneath dirfd without following symlinks or crossing mounts.
229+
func resolve(dirfd int, rel string) (int, error) {
230+
how := &unix.OpenHow{
231+
Flags: unix.O_PATH | unix.O_CLOEXEC,
232+
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
233+
}
234+
// openat2 reports a concurrent rename with EAGAIN; a sandbox that keeps renaming fails the view.
235+
for try := 0; ; try++ {
236+
fd, err := unix.Openat2(dirfd, rel, how)
237+
if (err == unix.EAGAIN || err == unix.EINTR) && try < 16 {
238+
continue
239+
}
240+
return fd, err
241+
}
242+
}
243+
244+
func newFS(fstype string, opts [][2]string, attr int) (int, error) {
245+
fs, err := unix.Fsopen(fstype, unix.FSOPEN_CLOEXEC)
246+
if err != nil {
247+
return -1, mountError("fsopen", fstype, err)
248+
}
249+
defer unix.Close(fs)
250+
for _, o := range opts {
251+
if err := unix.FsconfigSetString(fs, o[0], o[1]); err != nil {
252+
return -1, mountError("fsconfig "+o[0], fstype, err)
253+
}
254+
}
255+
if err := unix.FsconfigCreate(fs); err != nil {
256+
return -1, mountError("fsconfig create", fstype, err)
257+
}
258+
mnt, err := unix.Fsmount(fs, unix.FSMOUNT_CLOEXEC, attr)
259+
if err != nil {
260+
return -1, mountError("fsmount", fstype, err)
261+
}
262+
return mnt, nil
263+
}
264+
265+
func createFile(dirfd int, name, view string) error {
266+
fd, err := unix.Openat(dirfd, name, unix.O_CREAT|unix.O_EXCL|unix.O_WRONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o444)
267+
if err != nil {
268+
return &Error{Kind: ErrLauncher, Op: "create", Path: view, Err: err}
269+
}
270+
return unix.Close(fd)
271+
}
272+
273+
func readOnly(mnt int, view string) error {
274+
if err := unix.MountSetattr(mnt, "", unix.AT_EMPTY_PATH, &unix.MountAttr{Attr_set: unix.MOUNT_ATTR_RDONLY}); err != nil {
275+
return mountError("mount_setattr", view, err)
276+
}
277+
return nil
278+
}
279+
280+
func bindAttr(writable, exec, dev bool) *unix.MountAttr {
281+
a := &unix.MountAttr{Attr_set: attrNoSuid}
282+
for _, f := range []struct {
283+
set bool
284+
attr uint64
285+
}{{!writable, unix.MOUNT_ATTR_RDONLY}, {!exec, attrNoExec}, {!dev, attrNoDev}} {
286+
if f.set {
287+
a.Attr_set |= f.attr
288+
} else {
289+
a.Attr_clr |= f.attr
290+
}
291+
}
292+
return a
293+
}
294+
295+
// switchRoot makes the world root the process root. pivot_root would also detach the old root, but container seccomp profiles block it.
296+
func switchRoot(root int) error {
297+
if err := unix.Fchdir(root); err != nil {
298+
return &Error{Kind: ErrLauncher, Op: "fchdir", Path: "/", Err: err}
299+
}
300+
if err := unix.Mount(".", "/", "", unix.MS_MOVE, ""); err != nil {
301+
return mountError("move root", "/", err)
302+
}
303+
if err := unix.Chroot("."); err != nil {
304+
return &Error{Kind: ErrLauncher, Op: "chroot", Path: "/", Err: err}
305+
}
306+
if err := unix.Chdir("/"); err != nil {
307+
return &Error{Kind: ErrLauncher, Op: "chdir", Path: "/", Err: err}
308+
}
309+
return nil
310+
}
311+
312+
func mountError(op, path string, err error) error {
313+
kind := ErrLauncher
314+
if errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) {
315+
kind = ErrMountDenied
316+
}
317+
return &Error{Kind: kind, Op: op, Path: path, Err: err}
318+
}

0 commit comments

Comments
 (0)