返回 DeepSeek-Reasonix
write_roots_plan.go
根目录 / internal / sandbox / write_roots_plan.go
1 //go:build !windows
2
3 package sandbox
4
5 import (
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "sync"
11 )
12
13 // WriteRootRedirectedCode identifies a writable directory a launch left out
14 // because it resolves through a link inside a writable directory, where a
15 // confined command can re-point it.
16 const WriteRootRedirectedCode = "sandbox.write_root_redirected"
17
18 type writeCandidate struct {
19 key string
20 resolved string
21 links []string
22 caller bool
23 }
24
25 // writeRootPlan is the write surface one launch confines to: resolved paths
26 // in the order they were named, the caller's among them, and what it refused.
27 type writeRootPlan struct {
28 dirs []string
29 callers []string
30 refused []string
31 }
32
33 // planWriteRoots resolves the caller's roots and the backend's extra
34 // directories without following any link a confined command could have
35 // rewritten. A backend names the resolved paths, so a later swap cannot move
36 // a granted root.
37 func planWriteRoots(roots, extras []string, sessionTemp string) writeRootPlan {
38 var cands []writeCandidate
39 seen := map[string]bool{}
40 add := func(named string, caller bool) {
41 named = strings.TrimSpace(named)
42 if named == "" {
43 return
44 }
45 abs, err := filepath.Abs(named)
46 if err != nil || seen[abs] {
47 return
48 }
49 seen[abs] = true
50 resolved, links := resolveWithLinks(abs)
51 cands = append(cands, writeCandidate{key: abs, resolved: resolved, links: links, caller: caller})
52 }
53 for _, r := range roots {
54 add(r, true)
55 }
56 for _, r := range extras {
57 add(r, false)
58 }
59 regions := hostWritePins().paths()
60 if dir := strings.TrimSpace(sessionTemp); dir != "" {
61 resolved, _ := resolveWithLinks(dir)
62 regions = append(regions, resolved)
63 }
64 for _, c := range cands {
65 if len(c.links) == 0 {
66 regions = append(regions, c.resolved)
67 }
68 }
69 // A linked root widens the regions only once it passed against the
70 // unlinked ones, so a root re-pointed at / cannot refuse every other root.
71 for _, c := range cands {
72 if len(c.links) > 0 && !c.redirected(regions) {
73 regions = append(regions, c.resolved)
74 }
75 }
76 var plan writeRootPlan
77 kept := map[string]bool{}
78 for _, c := range cands {
79 if c.redirected(regions) {
80 plan.refused = append(plan.refused, c.resolved)
81 continue
82 }
83 if c.caller {
84 plan.callers = append(plan.callers, c.resolved)
85 }
86 if !kept[foldPath(c.resolved)] {
87 kept[foldPath(c.resolved)] = true
88 plan.dirs = append(plan.dirs, c.resolved)
89 }
90 }
91 return plan
92 }
93
94 // redirected reports whether c is not what it was, or resolves through a link
95 // a writable directory's own entry could be, or one inside it.
96 func (c writeCandidate) redirected(regions []string) bool {
97 for _, link := range c.links {
98 for _, region := range regions {
99 if PathWithin(foldPath(region), foldPath(link)) {
100 return true
101 }
102 }
103 }
104 return hostWritePins().changed(c.key, c.resolved)
105 }
106
107 // hostPins holds each host write directory's identity when first seen.
108 type hostPins map[string]hostIdentity
109
110 type hostIdentity struct {
111 path string
112 info os.FileInfo // nil when the directory did not exist
113 }
114
115 // hostWritePins pins the host write directories the first time this process
116 // confines a launch. A Seatbelt subpath covers the directory's own entry, so a
117 // confined command could otherwise leave a link where a cache was. Only the
118 // path is held: a cache rebuilt in place is the same grant.
119 var hostWritePins = pinHostWriteDirs
120
121 var pinHostWriteDirs = sync.OnceValue(func() hostPins {
122 pins := hostPins{}
123 for _, d := range append([]string{"/dev"}, hostWriteDirs()...) {
124 abs, err := filepath.Abs(strings.TrimSpace(d))
125 if err != nil || d == "" {
126 continue
127 }
128 resolved, _ := resolveWithLinks(abs)
129 pins[abs] = hostIdentity{path: resolved}
130 }
131 return pins
132 })
133
134 func (p hostPins) paths() []string {
135 out := make([]string, 0, len(p))
136 for _, id := range p {
137 out = append(out, id.path)
138 }
139 return out
140 }
141
142 func (p hostPins) changed(key, resolved string) bool {
143 id, ok := p[key]
144 if !ok {
145 return false
146 }
147 if foldPath(id.path) != foldPath(resolved) {
148 return true
149 }
150 if id.info == nil {
151 return false
152 }
153 info, err := os.Stat(resolved)
154 return err != nil || !os.SameFile(id.info, info)
155 }
156
157 func foldPath(p string) string {
158 if runtime.GOOS == "darwin" {
159 return strings.ToLower(p)
160 }
161 return p
162 }
163
163 lines GO