Monorepo for Tangled tangled.org
4

Configure Feed

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

1//go:build linux 2 3package sandbox 4 5import ( 6 "errors" 7 "fmt" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "syscall" 12 "unsafe" 13 14 "github.com/landlock-lsm/go-landlock/landlock" 15 "golang.org/x/sys/unix" 16) 17 18var ErrUnsupportedPlatform = errors.New("no sandbox backend available") 19 20// LandlockBackend uses the Linux Landlock LSM via a re-exec pattern. 21// landlock_restrict_self only affects the calling OS thread, so we re-exec 22// the binary as "sandbox-exec" which runs single-threaded before exec'ing git. 23type LandlockBackend struct { 24 selfExe string 25 lookup LookupUID 26} 27 28func (l *LandlockBackend) Wrap(repoPath string, cmd *exec.Cmd) (*exec.Cmd, error) { 29 return l.WrapMulti([]string{repoPath}, cmd) 30} 31 32func (l *LandlockBackend) WrapMulti(paths []string, cmd *exec.Cmd) (*exec.Cmd, error) { 33 if len(paths) == 0 { 34 return cmd, nil 35 } 36 37 // resolve the executable to an absolute path now, while $PATH is still 38 // intact; the re-exec'd sandbox-exec subprocess inherits the env we pass 39 // via cmd.Env, which may not include the wrappers that set up $PATH. 40 args := cmd.Args 41 if len(args) > 0 { 42 if abs, err := exec.LookPath(args[0]); err == nil { 43 args = append([]string{abs}, args[1:]...) 44 } 45 } 46 47 var sandboxArgs []string 48 sandboxArgs = append(sandboxArgs, "sandbox-exec") 49 for _, p := range paths { 50 sandboxArgs = append(sandboxArgs, "--repo-path="+p) 51 } 52 sandboxArgs = append(sandboxArgs, "--") 53 sandboxArgs = append(sandboxArgs, args...) 54 55 wrapped := exec.Command(l.selfExe, sandboxArgs...) 56 wrapped.Env = cmd.Env 57 wrapped.Dir = paths[0] // kernel chdir's here after setuid, before execve 58 wrapped.Stdin = cmd.Stdin 59 wrapped.Stdout = cmd.Stdout 60 wrapped.Stderr = cmd.Stderr 61 62 // drop to the virtual UID if we can resolve one. the kernel handles 63 // fork -> setresuid -> chdir -> execve; requires CAP_SETUID/GID on the caller. 64 // 65 // the primary GID is intentionally set to the virtual UID, NOT the 66 // repo's group ownership. repo dirs are owned by virtualUID:gitGroup 67 // with mode 0770 so the knot service (in gitGroup) can read them, but 68 // sandbox subprocesses must not inherit gitGroup or they would gain 69 // group access to every other repo and lose cross-owner isolation. 70 if l.lookup != nil { 71 if uid, _, err := l.lookup(paths[0]); err == nil && uid > 0 { 72 wrapped.SysProcAttr = &syscall.SysProcAttr{ 73 Credential: &syscall.Credential{Uid: uid, Gid: uid, NoSetGroups: true}, 74 } 75 } 76 } 77 78 return wrapped, nil 79} 80 81func (l *LandlockBackend) Name() string { return "landlock" } 82 83// RuleSpec describes the paths a sandbox should grant access to, grouped by 84// access tier. It is the input to the Landlock ruleset construction and is 85// exposed so the path-derivation logic can be tested independently of any 86// actual kernel-level enforcement. 87type RuleSpec struct { 88 // SystemRO is the set of system directories granted read+execute. 89 SystemRO []string 90 // GitConfigRO is the global git config file, granted read-only access 91 // at file granularity. Empty when $HOME is not set. 92 GitConfigRO string 93 // DevRW is the set of device-file directories granted read/write + 94 // ioctl access (needed so /dev/null works under Landlock V5+). 95 DevRW []string 96 // TmpRW is the set of directories granted read/write for temporary 97 // patch and object files. 98 TmpRW []string 99 // RepoRW is the set of repository directories granted read/write 100 // access including the REFER right (for cross-directory rename in 101 // receive-pack's quarantine migration). 102 RepoRW []string 103} 104 105// BuildRuleSpec derives the set of paths the sandbox should grant to each 106// access tier given the repository paths the subprocess operates on. 107func BuildRuleSpec(repoPaths []string) RuleSpec { 108 return buildRuleSpec(repoPaths, os.Getenv("HOME")) 109} 110 111// buildRuleSpec is the testable variant of BuildRuleSpec that takes $HOME 112// explicitly instead of reading it from the environment. 113func buildRuleSpec(repoPaths []string, home string) RuleSpec { 114 var gitConfig string 115 if home != "" { 116 // the only thing the sandboxed git subprocess needs from $HOME is the 117 // global config file. granting just that one file (not the whole 118 // .config tree) keeps everything else under $HOME outside the ruleset. 119 gitConfig = filepath.Join(home, ".config", "git", "config") 120 } 121 122 return RuleSpec{ 123 SystemRO: []string{"/usr", "/bin", "/lib", "/lib64", "/nix", "/etc"}, 124 GitConfigRO: gitConfig, 125 DevRW: []string{"/dev"}, 126 TmpRW: []string{"/tmp"}, 127 RepoRW: append([]string(nil), repoPaths...), 128 } 129} 130 131// ApplyLandlock applies a Landlock ruleset to the current process then 132// exec's into gitArgs. Called from the hidden "sandbox-exec" subcommand. 133func ApplyLandlock(repoPaths []string, gitArgs []string) error { 134 if len(gitArgs) == 0 { 135 return fmt.Errorf("sandbox-exec: no command specified") 136 } 137 138 spec := BuildRuleSpec(repoPaths) 139 140 rules := []landlock.Rule{ 141 landlock.RODirs(spec.SystemRO...).IgnoreIfMissing(), 142 landlock.RWFiles(spec.DevRW...).WithIoctlDev().IgnoreIfMissing(), 143 landlock.RWDirs(spec.TmpRW...).IgnoreIfMissing(), 144 } 145 if spec.GitConfigRO != "" { 146 rules = append(rules, landlock.ROFiles(spec.GitConfigRO).IgnoreIfMissing()) 147 } 148 for _, p := range spec.RepoRW { 149 rules = append(rules, landlock.RWDirs(p).WithRefer()) 150 } 151 152 // V8.BestEffort enforces the strongest ruleset the running kernel supports, 153 // up to V8. RestrictPaths also sets PR_SET_NO_NEW_PRIVS automatically. 154 if err := landlock.V8.BestEffort().RestrictPaths(rules...); err != nil { 155 return fmt.Errorf("sandbox-exec: restrict paths: %w", err) 156 } 157 158 gitBin := gitArgs[0] 159 if !filepath.IsAbs(gitBin) { 160 return fmt.Errorf("sandbox-exec: expected absolute path, got %q", gitBin) 161 } 162 163 return unix.Exec(gitBin, gitArgs, os.Environ()) 164} 165 166func probeLandlock() bool { 167 _, err := landlockCreateRuleset(nil, unix.LANDLOCK_CREATE_RULESET_VERSION) 168 // EOPNOTSUPP and ENOSYS mean the kernel doesn't support landlock. 169 // Any other result (including EINVAL for the nil attr) means it's available. 170 return !errors.Is(err, unix.EOPNOTSUPP) && !errors.Is(err, unix.ENOSYS) 171} 172 173func platformNew(lookup LookupUID) (Backend, string) { 174 if probeLandlock() { 175 selfExe, err := os.Readlink("/proc/self/exe") 176 if err != nil { 177 selfExe = "/proc/self/exe" 178 } 179 return &LandlockBackend{selfExe: selfExe, lookup: lookup}, "" 180 } 181 182 return &NoopBackend{}, "landlock unavailable (kernel < 5.13); git subprocesses run unsandboxed" 183} 184 185func platformProbe() string { 186 if probeLandlock() { 187 return "landlock available (kernel >= 5.13)" 188 } 189 return "no sandbox backend available (kernel < 5.13)" 190} 191 192// landlockCreateRuleset wraps the landlock_create_ruleset(2) syscall. 193// Pass attr=nil and flags=LANDLOCK_CREATE_RULESET_VERSION to query ABI version. 194// Used only for the non-destructive probe in probeLandlock; all ruleset 195// construction is handled by go-landlock. 196func landlockCreateRuleset(attr *unix.LandlockRulesetAttr, flags uint) (int, error) { 197 var attrPtr unsafe.Pointer 198 var attrSize uintptr 199 if attr != nil { 200 attrPtr = unsafe.Pointer(attr) 201 attrSize = unsafe.Sizeof(*attr) 202 } 203 fd, _, errno := unix.Syscall( 204 unix.SYS_LANDLOCK_CREATE_RULESET, 205 uintptr(attrPtr), 206 attrSize, 207 uintptr(flags), 208 ) 209 if errno != 0 { 210 return 0, errno 211 } 212 return int(fd), nil 213}