sandbox: decouple env resolution from process; plan9 namespace discovery
This commit is contained in:
parent
4188d5946b
commit
84533aed1a
|
|
@ -53,8 +53,11 @@ type AdvancedConfig struct {
|
|||
// defaultSandbox is the sandbox config used when none is specified
|
||||
const defaultSandbox = "default"
|
||||
|
||||
// expandPath replaces template variables with actual values
|
||||
func expandPath(pattern, cwd string) string {
|
||||
// EnvFunc looks up an environment variable by name.
|
||||
type EnvFunc func(string) string
|
||||
|
||||
// expandPath replaces template variables with actual values using getenv for lookups.
|
||||
func expandPath(pattern, cwd string, getenv EnvFunc) string {
|
||||
s := pattern
|
||||
s = strings.ReplaceAll(s, "{CWD}", cwd)
|
||||
|
||||
|
|
@ -62,39 +65,39 @@ func expandPath(pattern, cwd string) string {
|
|||
s = re.ReplaceAllStringFunc(s, func(match string) string {
|
||||
varName := match[1 : len(match)-1]
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
home := getenv("HOME")
|
||||
switch varName {
|
||||
case "HOME":
|
||||
if home != "" {
|
||||
return home
|
||||
}
|
||||
case "TMPDIR":
|
||||
if tmpdir := os.Getenv("TMPDIR"); tmpdir != "" {
|
||||
if tmpdir := getenv("TMPDIR"); tmpdir != "" {
|
||||
return tmpdir
|
||||
}
|
||||
return "/tmp"
|
||||
case "XDG_CONFIG_HOME":
|
||||
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
|
||||
if xdg := getenv("XDG_CONFIG_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".config")
|
||||
case "XDG_DATA_HOME":
|
||||
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
|
||||
if xdg := getenv("XDG_DATA_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".local/share")
|
||||
case "XDG_CACHE_HOME":
|
||||
if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||
if xdg := getenv("XDG_CACHE_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".cache")
|
||||
case "XDG_STATE_HOME":
|
||||
if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
|
||||
if xdg := getenv("XDG_STATE_HOME"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return filepath.Join(home, ".local/state")
|
||||
case "XDG_RUNTIME_DIR":
|
||||
if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
if xdg := getenv("XDG_RUNTIME_DIR"); xdg != "" {
|
||||
return xdg
|
||||
}
|
||||
return fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
|
|
@ -104,20 +107,20 @@ func expandPath(pattern, cwd string) string {
|
|||
return paths.DataDir()
|
||||
}
|
||||
|
||||
if val := os.Getenv(varName); val != "" {
|
||||
if val := getenv(varName); val != "" {
|
||||
return val
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
s = paths.ExpandHome(os.ExpandEnv(s))
|
||||
s = paths.ExpandHome(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// checkPath checks if the given absolute path is allowed by the sandbox config.
|
||||
// If write is true, the path must fall under an RW or RWX entry.
|
||||
// If write is false, any entry (RO, ROX, RW, RWX) grants access.
|
||||
func checkPath(cfg *Config, path string, write bool, cwd string) error {
|
||||
func checkPath(cfg *Config, path string, write bool, cwd string, getenv EnvFunc) error {
|
||||
cleaned := filepath.Clean(path)
|
||||
if resolved, err := filepath.EvalSymlinks(cleaned); err == nil {
|
||||
cleaned = resolved
|
||||
|
|
@ -130,17 +133,17 @@ func checkPath(cfg *Config, path string, write bool, cwd string) error {
|
|||
|
||||
var allowed []string
|
||||
for _, p := range cfg.Filesystem.RW {
|
||||
allowed = append(allowed, expandPath(p, cwd))
|
||||
allowed = append(allowed, expandPath(p, cwd, getenv))
|
||||
}
|
||||
for _, p := range cfg.Filesystem.RWX {
|
||||
allowed = append(allowed, expandPath(p, cwd))
|
||||
allowed = append(allowed, expandPath(p, cwd, getenv))
|
||||
}
|
||||
if !write {
|
||||
for _, p := range cfg.Filesystem.RO {
|
||||
allowed = append(allowed, expandPath(p, cwd))
|
||||
allowed = append(allowed, expandPath(p, cwd, getenv))
|
||||
}
|
||||
for _, p := range cfg.Filesystem.ROX {
|
||||
allowed = append(allowed, expandPath(p, cwd))
|
||||
allowed = append(allowed, expandPath(p, cwd, getenv))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ func TestExpandPath(t *testing.T) {
|
|||
for k, v := range tc.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
got := expandPath(tc.pattern, tc.cwd)
|
||||
got := expandPath(tc.pattern, tc.cwd, os.Getenv)
|
||||
if got != tc.want {
|
||||
t.Errorf("expandPath(%q, %q) = %q, want %q", tc.pattern, tc.cwd, got, tc.want)
|
||||
}
|
||||
|
|
@ -325,7 +325,7 @@ func TestCheckPath(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := checkPath(cfg, tc.path, tc.write, tmpDir)
|
||||
err := checkPath(cfg, tc.path, tc.write, tmpDir, os.Getenv)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("checkPath(%q, write=%v) error = %v, wantErr %v", tc.path, tc.write, err, tc.wantErr)
|
||||
}
|
||||
|
|
@ -334,7 +334,7 @@ func TestCheckPath(t *testing.T) {
|
|||
|
||||
t.Run("CWD template resolved", func(t *testing.T) {
|
||||
cfg2 := &Config{Filesystem: FilesystemConfig{RW: []string{"{CWD}"}}}
|
||||
err := checkPath(cfg2, filepath.Join(tmpDir, "newfile.txt"), true, tmpDir)
|
||||
err := checkPath(cfg2, filepath.Join(tmpDir, "newfile.txt"), true, tmpDir, os.Getenv)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -346,7 +346,7 @@ func TestCheckPath(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
cfg3 := &Config{Filesystem: FilesystemConfig{RWX: []string{rwxDir}}}
|
||||
if err := checkPath(cfg3, filepath.Join(rwxDir, "bin"), true, tmpDir); err != nil {
|
||||
if err := checkPath(cfg3, filepath.Join(rwxDir, "bin"), true, tmpDir, os.Getenv); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
|
@ -469,7 +469,7 @@ env:
|
|||
// ---- expandPath: remaining branches ----
|
||||
|
||||
func TestExpandPath_HOME(t *testing.T) {
|
||||
got := expandPath("{HOME}/test", "/cwd")
|
||||
got := expandPath("{HOME}/test", "/cwd", os.Getenv)
|
||||
home, _ := os.UserHomeDir()
|
||||
if got != home+"/test" {
|
||||
t.Errorf("expandPath({HOME}/test) = %q; want %q", got, home+"/test")
|
||||
|
|
@ -489,7 +489,7 @@ func TestExpandPath_XDG_Defaults(t *testing.T) {
|
|||
} {
|
||||
t.Run(tc.varName+"_default", func(t *testing.T) {
|
||||
t.Setenv(tc.varName, "")
|
||||
got := expandPath(tc.pattern, "/cwd")
|
||||
got := expandPath(tc.pattern, "/cwd", os.Getenv)
|
||||
if got != tc.want {
|
||||
t.Errorf("expandPath(%q) = %q; want %q", tc.pattern, got, tc.want)
|
||||
}
|
||||
|
|
@ -508,7 +508,7 @@ func TestExpandPath_XDG_FromEnv(t *testing.T) {
|
|||
} {
|
||||
t.Run(tc.varName+"_env", func(t *testing.T) {
|
||||
t.Setenv(tc.varName, "/custom/"+tc.varName)
|
||||
got := expandPath(tc.pattern, "/cwd")
|
||||
got := expandPath(tc.pattern, "/cwd", os.Getenv)
|
||||
if got != "/custom/"+tc.varName {
|
||||
t.Errorf("expandPath(%q) = %q; want /custom/%s", tc.pattern, got, tc.varName)
|
||||
}
|
||||
|
|
@ -517,11 +517,11 @@ func TestExpandPath_XDG_FromEnv(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExpandPath_OlliePaths(t *testing.T) {
|
||||
got1 := expandPath("{OLLIE_CFG_PATH}", "/cwd")
|
||||
got1 := expandPath("{OLLIE_CFG_PATH}", "/cwd", os.Getenv)
|
||||
if got1 == "{OLLIE_CFG_PATH}" || got1 == "" {
|
||||
t.Errorf("OLLIE_CFG_PATH not expanded: %q", got1)
|
||||
}
|
||||
got2 := expandPath("{OLLIE_DATA_PATH}", "/cwd")
|
||||
got2 := expandPath("{OLLIE_DATA_PATH}", "/cwd", os.Getenv)
|
||||
if got2 == "{OLLIE_DATA_PATH}" || got2 == "" {
|
||||
t.Errorf("OLLIE_DATA_PATH not expanded: %q", got2)
|
||||
}
|
||||
|
|
@ -541,7 +541,7 @@ func TestCheckPath_SymlinkResolved(t *testing.T) {
|
|||
|
||||
cfg := &Config{Filesystem: FilesystemConfig{RW: []string{realDir}}}
|
||||
// Access via symlink — EvalSymlinks resolves link/file to real/file
|
||||
if err := checkPath(cfg, filepath.Join(link, "file"), true, tmpDir); err != nil {
|
||||
if err := checkPath(cfg, filepath.Join(link, "file"), true, tmpDir, os.Getenv); err != nil {
|
||||
t.Errorf("symlink path should be allowed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -552,10 +552,10 @@ func TestCheckPath_ROX_ReadAllowed(t *testing.T) {
|
|||
os.MkdirAll(roxDir, 0755)
|
||||
|
||||
cfg := &Config{Filesystem: FilesystemConfig{ROX: []string{roxDir}}}
|
||||
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), false, tmpDir); err != nil {
|
||||
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), false, tmpDir, os.Getenv); err != nil {
|
||||
t.Errorf("ROX read should be allowed: %v", err)
|
||||
}
|
||||
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), true, tmpDir); err == nil {
|
||||
if err := checkPath(cfg, filepath.Join(roxDir, "bin"), true, tmpDir, os.Getenv); err == nil {
|
||||
t.Error("ROX write should be denied")
|
||||
}
|
||||
}
|
||||
|
|
@ -581,7 +581,7 @@ func TestWrapCommand_LandrunUnavailable(t *testing.T) {
|
|||
defer func() { isAvailableFn = old }()
|
||||
|
||||
cfg := &Config{}
|
||||
_, err := WrapCommand(cfg, []string{"echo", "hi"}, "/tmp")
|
||||
_, err := WrapCommand(cfg, []string{"echo", "hi"}, "/tmp", os.Getenv)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when landrun unavailable")
|
||||
}
|
||||
|
|
@ -696,7 +696,7 @@ func TestIsSuperpowerdRunning_SocketExists(t *testing.T) {
|
|||
|
||||
func mustWrapCommand(t *testing.T, cfg *Config, cmd []string, cwd string) []string {
|
||||
t.Helper()
|
||||
got, err := WrapCommand(cfg, cmd, cwd)
|
||||
got, err := WrapCommand(cfg, cmd, cwd, os.Getenv)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapCommand failed: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ var ErrNotAvailable = fmt.Errorf("SANDBOX FAILURE: landrun is not installed or n
|
|||
|
||||
// WrapCommand wraps a command with landrun based on the configuration.
|
||||
// Returns an error if landrun is not available.
|
||||
func WrapCommand(cfg *Config, originalCmd []string, cwd string) ([]string, error) {
|
||||
func WrapCommand(cfg *Config, originalCmd []string, cwd string, getenv EnvFunc) ([]string, error) {
|
||||
if !isAvailableFn() {
|
||||
return nil, ErrNotAvailable
|
||||
}
|
||||
|
|
@ -50,25 +50,25 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) ([]string, error
|
|||
|
||||
var entries []pathEntry
|
||||
for _, path := range cfg.Filesystem.RO {
|
||||
expanded := expandPath(path, cwd)
|
||||
expanded := expandPath(path, cwd, getenv)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--ro"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.ROX {
|
||||
expanded := expandPath(path, cwd)
|
||||
expanded := expandPath(path, cwd, getenv)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rox"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.RW {
|
||||
expanded := expandPath(path, cwd)
|
||||
expanded := expandPath(path, cwd, getenv)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rw"})
|
||||
}
|
||||
}
|
||||
for _, path := range cfg.Filesystem.RWX {
|
||||
expanded := expandPath(path, cwd)
|
||||
expanded := expandPath(path, cwd, getenv)
|
||||
if pathExists(expanded) {
|
||||
entries = append(entries, pathEntry{expanded, "--rwx"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,10 +585,29 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
|
|||
default:
|
||||
return "", fmt.Errorf("unsupported language: %s (supported: bash, python3, perl, awk, sed, ed, jq, expect, bc, lua)", language)
|
||||
}
|
||||
e.envMu.RLock()
|
||||
envMap := make(map[string]string, len(e.envExtra))
|
||||
for k, v := range e.envExtra {
|
||||
envMap[k] = v
|
||||
}
|
||||
e.envMu.RUnlock()
|
||||
for _, ev := range os.Environ() {
|
||||
k, v, _ := strings.Cut(ev, "=")
|
||||
if _, ok := envMap[k]; !ok {
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
if _, ok := envMap["NAMESPACE"]; !ok {
|
||||
if ns := plan9Namespace(envMap); ns != "" {
|
||||
envMap["NAMESPACE"] = ns
|
||||
}
|
||||
}
|
||||
getenv := func(key string) string { return envMap[key] }
|
||||
|
||||
if e.Yolo {
|
||||
cmd = exec.CommandContext(ctx, interpreter[0], interpreter[1:]...)
|
||||
} else {
|
||||
wrapped, wrapErr := sandbox.WrapCommand(cfg, interpreter, workDir)
|
||||
wrapped, wrapErr := sandbox.WrapCommand(cfg, interpreter, workDir, getenv)
|
||||
if wrapErr != nil {
|
||||
return "", wrapErr
|
||||
}
|
||||
|
|
@ -668,4 +687,28 @@ func prependOlliePath(env []string, ollie string) []string {
|
|||
return result
|
||||
}
|
||||
|
||||
// plan9Namespace computes the plan9port namespace directory from the environment.
|
||||
// Convention: $NAMESPACE if set, else /tmp/ns.<user>.<display> where display is
|
||||
// $DISPLAY with trailing .0 stripped and / replaced by _.
|
||||
func plan9Namespace(env map[string]string) string {
|
||||
if ns := env["NAMESPACE"]; ns != "" {
|
||||
return ns
|
||||
}
|
||||
user := env["USER"]
|
||||
if user == "" {
|
||||
return ""
|
||||
}
|
||||
disp := env["DISPLAY"]
|
||||
if disp == "" {
|
||||
return ""
|
||||
}
|
||||
// Canonicalize: strip trailing .0
|
||||
if strings.HasSuffix(disp, ".0") {
|
||||
disp = disp[:len(disp)-2]
|
||||
}
|
||||
// Replace / with _
|
||||
disp = strings.ReplaceAll(disp, "/", "_")
|
||||
return "/tmp/ns." + user + "." + disp
|
||||
}
|
||||
|
||||
var _ tools.Server = (*Server)(nil) // compile-time interface check
|
||||
|
|
|
|||
Reference in New Issue