This repository has no description
0

Configure Feed

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

core / knotserver / sandbox / sandbox_linux.go
7.5 kB 223 lines
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 -> setgroups -> setresgid -> setresuid -> chdir -> execve; 64 // requires CAP_SETUID/CAP_SETGID on the caller. 65 // 66 // the primary GID is intentionally set to the virtual UID, NOT the 67 // repo's group ownership. repo dirs are owned by virtualUID:gitGroup 68 // with mode 0770 so the knot service (in gitGroup) can read them, but 69 // sandbox subprocesses must not inherit gitGroup or they would gain 70 // group access to every other repo and lose cross-owner isolation. 71 // 72 // Groups is an empty (non-nil) slice and NoSetGroups is false so the 73 // kernel calls setgroups(0, NULL) and clears supplementary groups. 74 // NoSetGroups: true would skip setgroups entirely and the subprocess 75 // would inherit the parent's supplementary groups (including gitGroup). 76 if l.lookup != nil { 77 if uid, _, err := l.lookup(paths[0]); err == nil && uid > 0 { 78 wrapped.SysProcAttr = &syscall.SysProcAttr{ 79 Credential: &syscall.Credential{ 80 Uid: uid, 81 Gid: uid, 82 Groups: []uint32{}, 83 }, 84 } 85 } 86 } 87 88 return wrapped, nil 89} 90 91func (l *LandlockBackend) Name() string { return "landlock" } 92 93// RuleSpec describes the paths a sandbox should grant access to, grouped by 94// access tier. It is the input to the Landlock ruleset construction and is 95// exposed so the path-derivation logic can be tested independently of any 96// actual kernel-level enforcement. 97type RuleSpec struct { 98 // SystemRO is the set of system directories granted read+execute. 99 SystemRO []string 100 // GitConfigRO is the global git config file, granted read-only access 101 // at file granularity. Empty when $HOME is not set. 102 GitConfigRO string 103 // DevRW is the set of device-file directories granted read/write + 104 // ioctl access (needed so /dev/null works under Landlock V5+). 105 DevRW []string 106 // TmpRW is the set of directories granted read/write for temporary 107 // patch and object files. 108 TmpRW []string 109 // RepoRW is the set of repository directories granted read/write 110 // access including the REFER right (for cross-directory rename in 111 // receive-pack's quarantine migration). 112 RepoRW []string 113} 114 115// BuildRuleSpec derives the set of paths the sandbox should grant to each 116// access tier given the repository paths the subprocess operates on. 117func BuildRuleSpec(repoPaths []string) RuleSpec { 118 return buildRuleSpec(repoPaths, os.Getenv("HOME")) 119} 120 121// buildRuleSpec is the testable variant of BuildRuleSpec that takes $HOME 122// explicitly instead of reading it from the environment. 123func buildRuleSpec(repoPaths []string, home string) RuleSpec { 124 var gitConfig string 125 if home != "" { 126 // the only thing the sandboxed git subprocess needs from $HOME is the 127 // global config file. granting just that one file (not the whole 128 // .config tree) keeps everything else under $HOME outside the ruleset. 129 gitConfig = filepath.Join(home, ".config", "git", "config") 130 } 131 132 return RuleSpec{ 133 SystemRO: []string{"/usr", "/bin", "/lib", "/lib64", "/nix", "/etc"}, 134 GitConfigRO: gitConfig, 135 DevRW: []string{"/dev"}, 136 TmpRW: []string{"/tmp"}, 137 RepoRW: append([]string(nil), repoPaths...), 138 } 139} 140 141// ApplyLandlock applies a Landlock ruleset to the current process then 142// exec's into gitArgs. Called from the hidden "sandbox-exec" subcommand. 143func ApplyLandlock(repoPaths []string, gitArgs []string) error { 144 if len(gitArgs) == 0 { 145 return fmt.Errorf("sandbox-exec: no command specified") 146 } 147 148 spec := BuildRuleSpec(repoPaths) 149 150 rules := []landlock.Rule{ 151 landlock.RODirs(spec.SystemRO...).IgnoreIfMissing(), 152 landlock.RWFiles(spec.DevRW...).WithIoctlDev().IgnoreIfMissing(), 153 landlock.RWDirs(spec.TmpRW...).IgnoreIfMissing(), 154 } 155 if spec.GitConfigRO != "" { 156 rules = append(rules, landlock.ROFiles(spec.GitConfigRO).IgnoreIfMissing()) 157 } 158 for _, p := range spec.RepoRW { 159 rules = append(rules, landlock.RWDirs(p).WithRefer()) 160 } 161 162 // V8.BestEffort enforces the strongest ruleset the running kernel supports, 163 // up to V8. RestrictPaths also sets PR_SET_NO_NEW_PRIVS automatically. 164 if err := landlock.V8.BestEffort().RestrictPaths(rules...); err != nil { 165 return fmt.Errorf("sandbox-exec: restrict paths: %w", err) 166 } 167 168 gitBin := gitArgs[0] 169 if !filepath.IsAbs(gitBin) { 170 return fmt.Errorf("sandbox-exec: expected absolute path, got %q", gitBin) 171 } 172 173 return unix.Exec(gitBin, gitArgs, os.Environ()) 174} 175 176func probeLandlock() bool { 177 _, err := landlockCreateRuleset(nil, unix.LANDLOCK_CREATE_RULESET_VERSION) 178 // EOPNOTSUPP and ENOSYS mean the kernel doesn't support landlock. 179 // Any other result (including EINVAL for the nil attr) means it's available. 180 return !errors.Is(err, unix.EOPNOTSUPP) && !errors.Is(err, unix.ENOSYS) 181} 182 183func platformNew(lookup LookupUID) (Backend, string) { 184 if probeLandlock() { 185 selfExe, err := os.Readlink("/proc/self/exe") 186 if err != nil { 187 selfExe = "/proc/self/exe" 188 } 189 return &LandlockBackend{selfExe: selfExe, lookup: lookup}, "" 190 } 191 192 return &NoopBackend{}, "landlock unavailable (kernel < 5.13); git subprocesses run unsandboxed" 193} 194 195func platformProbe() string { 196 if probeLandlock() { 197 return "landlock available (kernel >= 5.13)" 198 } 199 return "no sandbox backend available (kernel < 5.13)" 200} 201 202// landlockCreateRuleset wraps the landlock_create_ruleset(2) syscall. 203// Pass attr=nil and flags=LANDLOCK_CREATE_RULESET_VERSION to query ABI version. 204// Used only for the non-destructive probe in probeLandlock; all ruleset 205// construction is handled by go-landlock. 206func landlockCreateRuleset(attr *unix.LandlockRulesetAttr, flags uint) (int, error) { 207 var attrPtr unsafe.Pointer 208 var attrSize uintptr 209 if attr != nil { 210 attrPtr = unsafe.Pointer(attr) 211 attrSize = unsafe.Sizeof(*attr) 212 } 213 fd, _, errno := unix.Syscall( 214 unix.SYS_LANDLOCK_CREATE_RULESET, 215 uintptr(attrPtr), 216 attrSize, 217 uintptr(flags), 218 ) 219 if errno != 0 { 220 return 0, errno 221 } 222 return int(fd), nil 223}