| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | |
| 8 | "reasonix/internal/sandbox" |
| 9 | ) |
| 10 | |
| 11 | // newSingleProgram digests a single program path with the binary's content, so |
| 12 | // a program swapped at the same path needs approval again at the next load. |
| 13 | func newSingleProgram(kind ProjectProgramKind, key, path, ws string) ProjectProgram { |
| 14 | p := NewProjectProgram(kind, key, path, ws, ws, path, nil) |
| 15 | if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && filepath.IsAbs(path) { |
| 16 | p.Files = []string{filepath.Clean(path)} |
| 17 | p.Digest = p.currentDigest() |
| 18 | } |
| 19 | return p |
| 20 | } |
| 21 | |
| 22 | // namesWritablePath reports a program path that is relative, climbs with "..", |
| 23 | // or lies — existing or not — in the workspace, an allow_write root, or a |
| 24 | // temporary or toolchain cache directory the bash jail leaves writable. A bare |
| 25 | // command name is looked up on PATH, which never searches the workspace. |
| 26 | func (c *Config) namesWritablePath(ws, path string) bool { |
| 27 | path = strings.TrimSpace(path) |
| 28 | if path == "" || !strings.ContainsAny(path, `/\`) { |
| 29 | return false |
| 30 | } |
| 31 | if !filepath.IsAbs(path) || strings.Contains(path, "..") || ws == "" { |
| 32 | return true |
| 33 | } |
| 34 | return inRoots(path, c.writableRoots(ws)) |
| 35 | } |
| 36 | |
| 37 | // inRoots reports path at or under any of roots, by spelling or by identity. |
| 38 | func inRoots(path string, roots []string) bool { |
| 39 | resolved, err := evalSymlinksAllowMissing(path) |
| 40 | if err != nil { |
| 41 | return true |
| 42 | } |
| 43 | for _, root := range roots { |
| 44 | if real, err := evalSymlinksAllowMissing(root); err == nil { |
| 45 | root = real |
| 46 | } |
| 47 | if pathWithinRoot(root, resolved) || sameFileAncestor(root, resolved) { |
| 48 | return true |
| 49 | } |
| 50 | } |
| 51 | return false |
| 52 | } |
| 53 | |
| 54 | func hostWritableDirs() []string { return sandbox.HostWritableDirs() } |
| 55 | |
| 56 | func (c *Config) writableRoots(ws string) []string { |
| 57 | roots := append([]string{ws, os.TempDir()}, hostWritableDirs()...) |
| 58 | for _, dir := range c.Sandbox.AllowWrite { |
| 59 | if dir = ExpandVars(dir); dir != "" { |
| 60 | roots = append(roots, dir) |
| 61 | } |
| 62 | } |
| 63 | return roots |
| 64 | } |
| 65 | |
| 66 | // sameFileAncestor asks the filesystem, not the spelling, whether root is path |
| 67 | // or one of its ancestors: a case-insensitive volume or a junction spells the |
| 68 | // same directory differently. |
| 69 | func sameFileAncestor(root, path string) bool { |
| 70 | rootInfo, err := os.Stat(root) |
| 71 | if err != nil { |
| 72 | return false |
| 73 | } |
| 74 | for dir := path; ; { |
| 75 | if info, err := os.Stat(dir); err == nil && os.SameFile(rootInfo, info) { |
| 76 | return true |
| 77 | } |
| 78 | parent := filepath.Dir(dir) |
| 79 | if parent == dir { |
| 80 | return false |
| 81 | } |
| 82 | dir = parent |
| 83 | } |
| 84 | } |
| 85 |