Monorepo for Tangled
tangled.org
1//go:build linux
2
3package microvm
4
5import (
6 "bufio"
7 "fmt"
8 "io"
9 "path/filepath"
10 "regexp"
11 "strconv"
12 "strings"
13)
14
15type narinfo struct {
16 StorePath string
17 URL string
18 NarHash string
19 NarSize int64
20 // paths this path depends on
21 References []string
22}
23
24const (
25 maxNarinfoSize = 1 << 20 // 1 MiB
26 storePrefix = "/nix/store/"
27 maxNarinfoLineLen = maxNarinfoSize
28)
29
30var nixStorePathBaseRe = regexp.MustCompile(`^[0-9abcdfghijklmnpqrsvwxyz]{32}-[^/]+$`)
31
32// parseNarinfo parses and validates a narinfo body.
33// - required fields must be present
34// - StorePath must be under /nix/store/
35// - URL must be a relative, traversal-safe path referencing a NAR in the
36// same staging cache
37// - NarSize must be a non-negative integer
38func parseNarinfo(r io.Reader) (*narinfo, error) {
39 lr := io.LimitReader(r, maxNarinfoSize+1)
40 scanner := bufio.NewScanner(lr)
41 scanner.Buffer(make([]byte, 4096), maxNarinfoLineLen)
42
43 var info narinfo
44 for scanner.Scan() {
45 line := scanner.Text()
46 if line == "" {
47 continue
48 }
49 key, value, ok := strings.Cut(line, ":")
50 if !ok {
51 return nil, fmt.Errorf("invalid narinfo line %q", line)
52 }
53 key = strings.TrimSpace(key)
54 value = strings.TrimSpace(value)
55
56 switch key {
57 case "StorePath":
58 info.StorePath = value
59 case "URL":
60 info.URL = value
61 case "NarHash":
62 info.NarHash = value
63 case "NarSize":
64 n, err := strconv.ParseInt(value, 10, 64)
65 if err != nil {
66 return nil, fmt.Errorf("invalid NarSize %q: %w", value, err)
67 }
68 info.NarSize = n
69 case "References":
70 info.References = strings.Fields(value)
71 }
72 }
73 if err := scanner.Err(); err != nil {
74 return nil, fmt.Errorf("read narinfo: %w", err)
75 }
76
77 if err := validateNarinfo(&info); err != nil {
78 return nil, err
79 }
80 return &info, nil
81}
82
83func validateNarinfo(info *narinfo) error {
84 if info.StorePath == "" {
85 return fmt.Errorf("narinfo missing StorePath")
86 }
87 if _, _, err := parseStorePath(info.StorePath); err != nil {
88 return fmt.Errorf("invalid StorePath: %w", err)
89 }
90 if info.URL == "" {
91 return fmt.Errorf("narinfo missing URL")
92 }
93 if strings.HasPrefix(info.URL, "/") || strings.Contains(info.URL, "..") {
94 return fmt.Errorf("narinfo URL %q is not a safe relative path", info.URL)
95 }
96 if !strings.HasPrefix(info.URL, "nar/") {
97 return fmt.Errorf("narinfo URL %q must reference a staged nar/ object", info.URL)
98 }
99 name := strings.TrimPrefix(info.URL, "nar/")
100 if name == "" || name == "." || name != filepath.Base(name) || strings.Contains(name, "/") {
101 return fmt.Errorf("narinfo URL %q is not a safe nar object path", info.URL)
102 }
103 if info.NarHash == "" {
104 return fmt.Errorf("narinfo missing NarHash")
105 }
106 if info.NarSize < 0 {
107 return fmt.Errorf("narinfo NarSize must be non-negative")
108 }
109 return nil
110}
111
112func parseStorePath(path string) (hash string, name string, err error) {
113 if !strings.HasPrefix(path, storePrefix) {
114 return "", "", fmt.Errorf("store path %q does not start with %q", path, storePrefix)
115 }
116
117 base := strings.TrimPrefix(path, storePrefix)
118 if base == "" || strings.Contains(base, "/") {
119 return "", "", fmt.Errorf("store path %q has invalid base name", path)
120 }
121 if !nixStorePathBaseRe.MatchString(base) {
122 return "", "", fmt.Errorf("store path %q is not a valid nix store path", path)
123 }
124
125 hash, name, ok := strings.Cut(base, "-")
126 if !ok || hash == "" || name == "" {
127 return "", "", fmt.Errorf("store path %q is missing hash or name", path)
128 }
129 return hash, name, nil
130}