Monorepo for Tangled tangled.org
3

Configure Feed

Select the types of activity you want to include in your feed.

core / knotserver / sandbox / repofs.go
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}