|
| 1 | +//go:build unix |
| 2 | + |
| 3 | +package sandbox |
| 4 | + |
| 5 | +import ( |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "path/filepath" |
| 11 | + "syscall" |
| 12 | +) |
| 13 | + |
| 14 | +// RunChild is the entrypoint for the hidden `__sandbox_exec` subcommand. argv is |
| 15 | +// the command line after the `--` separator: argv[0] is the program to run and |
| 16 | +// argv[1:] are its arguments. It decodes the Spec from EnvSpec, applies the |
| 17 | +// confinement, performs a best-effort uid/gid drop, then execs the target, |
| 18 | +// replacing this process image — so the untrusted server inherits the locked |
| 19 | +// Landlock domain and its stdin/stdout/stderr stay wired straight through to the |
| 20 | +// parent with no mux. |
| 21 | +// |
| 22 | +// On success it never returns (execve replaces the image). It returns a non-zero |
| 23 | +// exit code on any failure; diag receives human-readable confinement notes and |
| 24 | +// errors (os.Stderr in production, so they land in the per-server upstream log). |
| 25 | +func RunChild(argv []string, diag io.Writer) int { |
| 26 | + if diag == nil { |
| 27 | + diag = io.Discard |
| 28 | + } |
| 29 | + if len(argv) == 0 { |
| 30 | + fmt.Fprintln(diag, "sandbox: no command to exec") |
| 31 | + return 2 |
| 32 | + } |
| 33 | + |
| 34 | + spec, ok, err := SpecFromEnv() |
| 35 | + if err != nil { |
| 36 | + fmt.Fprintln(diag, err) |
| 37 | + return 2 |
| 38 | + } |
| 39 | + if !ok { |
| 40 | + // The wrapper must never run a command unconfined just because the spec |
| 41 | + // went missing — that would silently defeat the isolation request. |
| 42 | + fmt.Fprintln(diag, "sandbox: missing spec env; refusing to run unconfined") |
| 43 | + return 2 |
| 44 | + } |
| 45 | + |
| 46 | + // Resolve the target before confinement so a bare command name can be looked |
| 47 | + // up on PATH while the filesystem is still fully visible. |
| 48 | + target := argv[0] |
| 49 | + if filepath.Base(target) == target { |
| 50 | + resolved, lerr := exec.LookPath(target) |
| 51 | + if lerr != nil { |
| 52 | + fmt.Fprintf(diag, "sandbox: lookup %q: %v\n", target, lerr) |
| 53 | + return 127 |
| 54 | + } |
| 55 | + target = resolved |
| 56 | + } |
| 57 | + |
| 58 | + rep, err := Apply(spec) |
| 59 | + if err != nil { |
| 60 | + // fail-closed: BestEffort was false and the primitive is unavailable. |
| 61 | + fmt.Fprintf(diag, "sandbox: confinement unavailable and fail-closed: %v\n", err) |
| 62 | + return 3 |
| 63 | + } |
| 64 | + fmt.Fprintf(diag, "sandbox: %s\n", describeReport(rep)) |
| 65 | + |
| 66 | + dropPrivilegesBestEffort(diag) |
| 67 | + |
| 68 | + if err := syscall.Exec(target, argv, os.Environ()); err != nil { |
| 69 | + fmt.Fprintf(diag, "sandbox: exec %q: %v\n", target, err) |
| 70 | + return 126 |
| 71 | + } |
| 72 | + return 0 // unreachable: Exec replaced the image on success. |
| 73 | +} |
| 74 | + |
| 75 | +// describeReport renders a one-line honest summary of what Apply enforced. |
| 76 | +func describeReport(rep Report) string { |
| 77 | + switch { |
| 78 | + case rep.LandlockABI >= 1: |
| 79 | + s := fmt.Sprintf("Landlock enforced (ABI %d), %d rlimit(s) set", rep.LandlockABI, rep.RlimitsSet) |
| 80 | + if rep.LandlockNote != "" { |
| 81 | + s += "; " + rep.LandlockNote |
| 82 | + } |
| 83 | + return s |
| 84 | + case rep.LandlockABI < 0: |
| 85 | + return fmt.Sprintf("running DEGRADED/unconfined — %s (%d rlimit(s) set)", rep.LandlockNote, rep.RlimitsSet) |
| 86 | + default: |
| 87 | + return fmt.Sprintf("%s (%d rlimit(s) set)", rep.LandlockNote, rep.RlimitsSet) |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +// dropPrivilegesBestEffort drops to the real uid/gid when the wrapper is running |
| 92 | +// as root with a non-root real user (e.g. a setuid/elevated launch). This is a |
| 93 | +// best-effort defense-in-depth step: in the personal edition mcpproxy runs as |
| 94 | +// the user, not root, so this is a documented no-op. A real, unconditional |
| 95 | +// privilege drop requires root/CAP_SETUID and an explicit target uid/gid, which |
| 96 | +// is out of scope here. |
| 97 | +func dropPrivilegesBestEffort(diag io.Writer) { |
| 98 | + euid, ruid := os.Geteuid(), os.Getuid() |
| 99 | + if euid != 0 || ruid == 0 { |
| 100 | + return // not privileged, or already the real user — nothing to drop. |
| 101 | + } |
| 102 | + rgid := os.Getgid() |
| 103 | + if err := syscall.Setgid(rgid); err != nil { |
| 104 | + fmt.Fprintf(diag, "sandbox: best-effort setgid(%d) failed: %v\n", rgid, err) |
| 105 | + return |
| 106 | + } |
| 107 | + if err := syscall.Setuid(ruid); err != nil { |
| 108 | + fmt.Fprintf(diag, "sandbox: best-effort setuid(%d) failed: %v\n", ruid, err) |
| 109 | + return |
| 110 | + } |
| 111 | + fmt.Fprintf(diag, "sandbox: dropped privileges to uid=%d gid=%d\n", ruid, rgid) |
| 112 | +} |
0 commit comments