| 1 | // Package rootstate opens local authority through root-owned directories. |
| 2 | package rootstate |
| 3 | |
| 4 | import ( |
| 5 | "context" |
| 6 | "os" |
| 7 | "path" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | const MaxRecord = 1 << 20 |
| 12 | |
| 13 | // Store retains the directory descriptor. Close it after the last operation. |
| 14 | type Store interface { |
| 15 | Read(string) ([]byte, error) |
| 16 | Write(string, []byte) error |
| 17 | Remove(string) error |
| 18 | Names() ([]string, error) |
| 19 | Lock(context.Context, string) (func(), error) |
| 20 | Close() error |
| 21 | } |
| 22 | |
| 23 | // Sync makes a checked directory's visible entries durable. A read-only |
| 24 | // projection consumer uses it before acknowledging a newly observed record. |
| 25 | func Sync(s Store) error { |
| 26 | if d, ok := s.(interface{ Sync() error }); ok { |
| 27 | return d.Sync() |
| 28 | } |
| 29 | return os.ErrPermission |
| 30 | } |
| 31 | |
| 32 | func component(s string) bool { |
| 33 | if s == "" || s == "." || s == ".." || len(s) > 255 { |
| 34 | return false |
| 35 | } |
| 36 | for _, r := range s { |
| 37 | if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_') { |
| 38 | return false |
| 39 | } |
| 40 | } |
| 41 | return true |
| 42 | } |
| 43 | |
| 44 | func fileMode(public bool) os.FileMode { |
| 45 | if public { |
| 46 | return 0644 |
| 47 | } |
| 48 | return 0600 |
| 49 | } |
| 50 | |
| 51 | type FileIdentity struct { |
| 52 | Device uint64 |
| 53 | Inode uint64 |
| 54 | Boot, Filesystem, Handle, MountRoot, MountPoint, MountNamespace string `json:",omitempty"` |
| 55 | MountLineage string `json:",omitempty"` |
| 56 | HandleType int32 `json:",omitempty"` |
| 57 | Mount uint64 `json:",omitempty"` |
| 58 | MountUnique, NamespaceUnique uint64 `json:",omitempty"` |
| 59 | UID, GID, Mode uint32 `json:",omitempty"` |
| 60 | } |
| 61 | |
| 62 | func (a FileIdentity) Same(b FileIdentity) bool { |
| 63 | if a.Boot == "" || b.Boot == "" { |
| 64 | return false |
| 65 | } |
| 66 | if a.Filesystem == "" || a.Handle == "" || a.MountRoot == "" || a.MountPoint == "" || a.MountNamespace == "" || b.MountNamespace == "" { |
| 67 | return false |
| 68 | } |
| 69 | if a.Inode != b.Inode || a.Filesystem != b.Filesystem || a.Handle != b.Handle || a.HandleType != b.HandleType || a.UID != b.UID || a.GID != b.GID || a.Mode != b.Mode || a.MountRoot != b.MountRoot || a.MountPoint != b.MountPoint { |
| 70 | return false |
| 71 | } |
| 72 | if (a.MountLineage != "" || b.MountLineage != "") && (a.MountUnique == 0 || b.MountUnique == 0 || a.NamespaceUnique == 0 || b.NamespaceUnique == 0) { |
| 73 | return false |
| 74 | } |
| 75 | if a.SameMountNamespace(b) && a.MountUnique != b.MountUnique { |
| 76 | return false |
| 77 | } |
| 78 | if a.MountLineage != b.MountLineage && (a.SameMountNamespace(b) || !sameMountLayers(a, b)) { |
| 79 | return false |
| 80 | } |
| 81 | return a.Boot != b.Boot || a.Device == b.Device |
| 82 | } |
| 83 | |
| 84 | func (a FileIdentity) SameMountNamespace(b FileIdentity) bool { |
| 85 | return a.Boot != "" && a.Boot == b.Boot && a.NamespaceUnique != 0 && a.NamespaceUnique == b.NamespaceUnique |
| 86 | } |
| 87 | |
| 88 | func sameMountLayers(a, b FileIdentity) bool { |
| 89 | layers := func(id FileIdentity) (string, []string, bool) { |
| 90 | if id.MountLineage == "" { |
| 91 | return "", nil, false |
| 92 | } |
| 93 | var out strings.Builder |
| 94 | rows := strings.Split(strings.TrimSuffix(id.MountLineage, "\n"), "\n") |
| 95 | for i, row := range rows { |
| 96 | fields := strings.Split(row, "\t") |
| 97 | if len(fields) != 2 { |
| 98 | return "", nil, false |
| 99 | } |
| 100 | if fields[1] != id.MountPoint { |
| 101 | return out.String(), rows[i:], out.Len() != 0 |
| 102 | } |
| 103 | out.WriteString(row + "\n") |
| 104 | } |
| 105 | return out.String(), nil, out.Len() != 0 |
| 106 | } |
| 107 | left, before, ok1 := layers(a) |
| 108 | right, after, ok2 := layers(b) |
| 109 | if !ok1 || !ok2 || left != right { |
| 110 | return false |
| 111 | } |
| 112 | for i, row := range before { |
| 113 | if len(after) != 0 && row == after[0] { |
| 114 | after = after[1:] |
| 115 | continue |
| 116 | } |
| 117 | fields := strings.Split(row, "\t") |
| 118 | if len(fields) != 2 || i+1 == len(before) || fields[1] == "/" { |
| 119 | return false |
| 120 | } |
| 121 | parent := strings.Split(before[i+1], "\t") |
| 122 | if len(parent) != 2 { |
| 123 | return false |
| 124 | } |
| 125 | for _, name := range append(fields, parent...) { |
| 126 | if !path.IsAbs(name) || path.Clean(name) != name { |
| 127 | return false |
| 128 | } |
| 129 | } |
| 130 | rel, below := strings.CutPrefix(fields[1], strings.TrimSuffix(parent[1], "/")+"/") |
| 131 | if !below || rel == "" || path.Join(parent[0], rel) != fields[0] { |
| 132 | return false |
| 133 | } |
| 134 | } |
| 135 | return len(after) == 0 |
| 136 | } |
| 137 | |