返回 DeepSeek-Reasonix
git_metadata_groups.go
根目录 / internal / sandbox / git_metadata_groups.go
1 package sandbox
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 )
10
11 // GitMetadataLinkedCode identifies a refusal to run confined because a
12 // protected Git metadata file has another hard link the sandbox cannot cover.
13 const GitMetadataLinkedCode = "sandbox.git_metadata_linked"
14
15 // ErrGitMetadataLinked is returned by CheckGitMetadata for that refusal.
16 var ErrGitMetadataLinked = errors.New(GitMetadataLinkedCode)
17
18 const (
19 // gitGroupMaxEntries bounds the worktree or submodule gitdirs given exact
20 // rules. Past it Seatbelt falls back to its patterns, so the profile cannot
21 // be grown by planting gitdirs.
22 gitGroupMaxEntries = 128
23 // gitGroupMaxMounts is the larger budget bubblewrap, which has no
24 // patterns, spends on exact mounts before binding a whole group read-only.
25 gitGroupMaxMounts = 512
26 gitGroupMaxDirs = 4096
27 gitGroupMaxDepth = 8
28 gitHooksMaxEntries = 256
29 )
30
31 // gitGroups is the metadata under a common dir's worktrees/ and modules/:
32 // exact paths for each gitdir found within the limit, and whether a group
33 // went past it. A group that is missing or not a real directory is skipped.
34 type gitGroups struct {
35 Paths []gitProtectedPath
36 Worktrees string
37 Modules string
38 WorktreesOver bool
39 ModulesOver bool
40 }
41
42 func gitGroupsOf(common string, limit int) gitGroups {
43 var g gitGroups
44 if dir := filepath.Join(common, gitWorktreesSubdir); isRealDir(dir) {
45 g.Worktrees = dir
46 entries, _ := os.ReadDir(dir)
47 n := 0
48 for _, e := range entries {
49 if !e.IsDir() {
50 continue
51 }
52 if n++; n > limit {
53 g.WorktreesOver = true
54 break
55 }
56 entry := filepath.Join(dir, e.Name())
57 g.Paths = append(g.Paths,
58 gitProtectedPath{Path: entry, Pin: true},
59 gitProtectedPath{Path: filepath.Join(entry, "config")},
60 gitProtectedPath{Path: filepath.Join(entry, "config.worktree")},
61 gitProtectedPath{Path: filepath.Join(entry, "commondir")})
62 }
63 }
64 if dir := filepath.Join(common, gitModulesSubdir); isRealDir(dir) {
65 g.Modules = dir
66 walk := moduleWalk{limit: limit}
67 walk.visit(dir, 0)
68 g.ModulesOver = walk.overflow
69 seen := map[string]bool{}
70 for _, gitDir := range walk.gitDirs {
71 for d := gitDir; d != dir && !seen[d]; d = filepath.Dir(d) {
72 seen[d] = true
73 g.Paths = append(g.Paths, gitProtectedPath{Path: d, Pin: true})
74 }
75 g.Paths = append(g.Paths, gitDirMetadata(gitDir)[1:]...)
76 }
77 }
78 return g
79 }
80
81 // gitGroupPaths is gitGroupsOf with each over-limit group protected as one
82 // read-only tree, for callers that have no patterns to fall back on.
83 func gitGroupPaths(common string, limit int) []gitProtectedPath {
84 g := gitGroupsOf(common, limit)
85 if g.WorktreesOver {
86 g.Paths = append(g.Paths, gitProtectedPath{Path: g.Worktrees, Tree: true})
87 }
88 if g.ModulesOver {
89 g.Paths = append(g.Paths, gitProtectedPath{Path: g.Modules, Tree: true})
90 }
91 return g.Paths
92 }
93
94 func isRealDir(path string) bool {
95 info, err := os.Lstat(path)
96 return err == nil && info.IsDir()
97 }
98
99 // moduleWalk finds submodule git directories, recognised by their HEAD file as
100 // git recognises a git directory, within a fixed budget. Entries are read
101 // without following symlinks, so a planted link cannot lead the walk away.
102 type moduleWalk struct {
103 limit int
104 gitDirs []string
105 dirs int
106 overflow bool
107 }
108
109 func (w *moduleWalk) visit(dir string, depth int) {
110 if w.overflow || depth > gitGroupMaxDepth {
111 return
112 }
113 entries, err := os.ReadDir(dir)
114 if err != nil {
115 return
116 }
117 for _, e := range entries {
118 if !e.IsDir() {
119 continue
120 }
121 if w.dirs++; w.dirs > gitGroupMaxDirs {
122 w.overflow = true
123 return
124 }
125 sub := filepath.Join(dir, e.Name())
126 if info, err := os.Lstat(filepath.Join(sub, "HEAD")); err == nil && info.Mode().IsRegular() {
127 if len(w.gitDirs) >= w.limit {
128 w.overflow = true
129 return
130 }
131 w.gitDirs = append(w.gitDirs, sub)
132 w.visit(filepath.Join(sub, gitModulesSubdir), depth+1)
133 continue
134 }
135 w.visit(sub, depth+1)
136 }
137 }
138
139 // CheckGitMetadata refuses a confined launch when a protected file has another
140 // hard link: a write through that name would change the file and no path
141 // rule can see it. Such a link predates the sandbox, which refuses making one.
142 func CheckGitMetadata(spec Spec) error {
143 linked := gitMetadataLinked(spec)
144 if len(linked) == 0 {
145 return nil
146 }
147 return fmt.Errorf("%w: %s has another hard link, so a write through that link would change host-protected Git metadata; "+
148 "the user has to remove the extra link outside the sandbox (find the workspace -samefile <path>) before commands run confined here",
149 ErrGitMetadataLinked, strings.Join(linked, ", "))
150 }
151
152 func gitMetadataLinked(spec Spec) []string {
153 meta := gitMetadataForSpec(spec)
154 paths := meta.Paths
155 for _, common := range meta.Commons {
156 paths = append(paths, gitGroupsOf(common, gitGroupMaxEntries).Paths...)
157 }
158 var linked []string
159 for _, p := range paths {
160 switch {
161 case p.Pin:
162 case p.Tree:
163 linked = append(linked, linkedFilesUnder(p.Path)...)
164 case isLinkedFile(p.Path):
165 linked = append(linked, p.Path)
166 }
167 }
168 return linked
169 }
170
171 func linkedFilesUnder(root string) []string {
172 var out []string
173 seen := 0
174 _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
175 if err != nil {
176 return nil
177 }
178 if seen++; seen > gitHooksMaxEntries {
179 return filepath.SkipAll
180 }
181 if !d.IsDir() && isLinkedFile(path) {
182 out = append(out, path)
183 }
184 return nil
185 })
186 return out
187 }
188
189 func isLinkedFile(path string) bool {
190 info, err := os.Lstat(path)
191 return err == nil && info.Mode().IsRegular() && linkCount(info) > 1
192 }
193
193 lines GO