Monorepo for Tangled
tangled.org
3.4 kB
108 lines
1package sandbox
2
3import (
4 "fmt"
5 "io/fs"
6 "os"
7 "path/filepath"
8 "sort"
9 "strings"
10 "syscall"
11)
12
13// ChmodRepoTree sets directory modes to 2770 (with the setgid bit) and
14// file modes to 0660 under root, preserving the executable bit on files
15// (hook scripts need it). Symlinks are skipped since their mode is not
16// meaningful.
17//
18// The group bits exist so the knot service (running as the git user, which
19// is in the git group that owns the repos) can still read and write the
20// repo via group permissions even though the repo's UID owner is a virtual
21// UID. Sandbox subprocesses drop supplementary groups so cross-owner
22// isolation still holds.
23//
24// The setgid bit on directories makes new files and subdirectories created
25// by sandbox subprocesses inherit the directory's group (the git group)
26// rather than the subprocess's primary group (the virtual UID). Without
27// it, sandbox-created files would be unreadable to the knot service.
28func ChmodRepoTree(root string) error {
29 return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
30 if err != nil {
31 return err
32 }
33 if d.Type()&fs.ModeSymlink != 0 {
34 return nil
35 }
36 if d.IsDir() {
37 return os.Chmod(path, 0770|os.ModeSetgid)
38 }
39 info, err := d.Info()
40 if err != nil {
41 return err
42 }
43 mode := fs.FileMode(0660)
44 if info.Mode()&0100 != 0 {
45 mode = 0770
46 }
47 return os.Chmod(path, mode)
48 })
49}
50
51// ChownRepoTree recursively chowns every entry under root to uid:gid.
52// Entries are processed deepest-first so a directory is only chowned after
53// its contents, preserving the calling process's access throughout the walk.
54// Call ChmodRepoTree first if you also want to tighten permissions; this
55// function only changes ownership.
56func ChownRepoTree(root string, uid int, gid int) error {
57 type entry struct {
58 path string
59 depth int
60 }
61 var entries []entry
62 if err := filepath.WalkDir(root, func(path string, _ fs.DirEntry, err error) error {
63 if err != nil {
64 return err
65 }
66 depth := strings.Count(path, string(filepath.Separator))
67 entries = append(entries, entry{path, depth})
68 return nil
69 }); err != nil {
70 return err
71 }
72
73 sort.Slice(entries, func(i, j int) bool {
74 return entries[i].depth > entries[j].depth
75 })
76
77 for _, e := range entries {
78 if err := os.Lchown(e.path, uid, gid); err != nil {
79 return err
80 }
81 }
82 return nil
83}
84
85// LookupUIDForRepoPath returns the owner UID and GID of the repo directory at
86// repoPath. scanPath is validated as a prefix to guard against directory escape.
87func LookupUIDForRepoPath(scanPath, repoPath string) (uid uint32, gid uint32, err error) {
88 if !strings.HasPrefix(repoPath, scanPath) {
89 return 0, 0, fmt.Errorf("repo path %q is outside scan path %q", repoPath, scanPath)
90 }
91 var stat syscall.Stat_t
92 if err := syscall.Stat(repoPath, &stat); err != nil {
93 return 0, 0, err
94 }
95 return stat.Uid, stat.Gid, nil
96}
97
98// ServiceGid returns the GID of scanPath, which is treated as the "service
99// group" that owns all repositories. Callers chown repo trees to
100// (virtualUID, ServiceGid(scanPath)) so the knot service (a member of this
101// group) retains read+write access via the group bits set by ChmodRepoTree.
102func ServiceGid(scanPath string) (uint32, error) {
103 var stat syscall.Stat_t
104 if err := syscall.Stat(scanPath, &stat); err != nil {
105 return 0, fmt.Errorf("stat %s: %w", scanPath, err)
106 }
107 return stat.Gid, nil
108}