| 1 | package cgroup |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "sync" |
| 8 | ) |
| 9 | |
| 10 | // The in-flight registry: target cgroup directories that a shim is being |
| 11 | // placed into, from just before its fork until it answers placed or the |
| 12 | // start fails. With clone3 a child appeared in its cgroup atomically; a |
| 13 | // shim spends a few milliseconds in the agent's cgroup first, so a proof |
| 14 | // that a cgroup is empty must count these too (Ruling 6, Task 2). The flat |
| 15 | // PID-move fallback (startAndMoveIn) registers its target as well, from |
| 16 | // its start until its PID write has returned. Readers: Cleanup's |
| 17 | // population and members (cleanup_linux.go) and ensureParent (gen.go). |
| 18 | // |
| 19 | // A registration ends only once its process is in the kernel's view of its |
| 20 | // cgroup (or never will be), so a placement is always visible to one of the |
| 21 | // two: the registry, or the kernel. But a reader that asks them one after |
| 22 | // the other can miss a placement that moves from one to the other between |
| 23 | // the two reads (whole-branch review, finding 1). Two tools close that: |
| 24 | // |
| 25 | // - readQuiet brackets one kernel read: it answers quiet only when no |
| 26 | // placement into the directory or below it was in flight at any moment |
| 27 | // of that read (none when it began, none registered or ended during it, |
| 28 | // none at its end). A reader that is not quiet treats the directory as |
| 29 | // populated, or its member list as unproven. Cleanup's reads use it. |
| 30 | // - holdPlacements stops any placement into the directory or below it |
| 31 | // from registering until it is released, so a decision taken under it |
| 32 | // (ensureParent's cold parent) holds until its writes are done. It is |
| 33 | // held across cgroupfs reads and writes only, never across a before |
| 34 | // callback, a shim wait or a start. |
| 35 | // |
| 36 | // No lock of the registry is ever held across a before callback, a shim |
| 37 | // wait or a start: placeMu guards only the maps, and a registration blocked |
| 38 | // by a hold waits on a channel with placeMu released. |
| 39 | |
| 40 | // placementReadHook, when set, runs right after each kernel read that |
| 41 | // readQuiet brackets, and after ensureParent's population read. Only test |
| 42 | // files set it, to register or end a placement at exactly that point. |
| 43 | var placementReadHook func(dir string) |
| 44 | |
| 45 | // placementHeldHook, when set, runs when a registration finds a hold over |
| 46 | // its target and is about to wait for it. Only test files set it. |
| 47 | var placementHeldHook func(dir string) |
| 48 | |
| 49 | // placeWindow is one readQuiet in progress: busy means a placement under |
| 50 | // dir was in flight when it began; changed, that one registered or ended |
| 51 | // under dir since. |
| 52 | type placeWindow struct { |
| 53 | dir string |
| 54 | busy bool |
| 55 | changed bool |
| 56 | } |
| 57 | |
| 58 | // beginPlacement registers one placement into dir. done ends it, and is |
| 59 | // idempotent: a second call does nothing. A nil manager registers nothing. |
| 60 | // While a hold covers dir (holdPlacements), it waits for the hold to end. |
| 61 | func (m *Manager) beginPlacement(dir string) (done func()) { |
| 62 | if m == nil { |
| 63 | return func() {} |
| 64 | } |
| 65 | dir = filepath.Clean(dir) |
| 66 | m.placeMu.Lock() |
| 67 | for m.heldLocked(dir) { |
| 68 | wake := m.wakeLocked() |
| 69 | m.placeMu.Unlock() |
| 70 | if placementHeldHook != nil { |
| 71 | placementHeldHook(dir) |
| 72 | } |
| 73 | <-wake |
| 74 | m.placeMu.Lock() |
| 75 | } |
| 76 | if m.placing == nil { |
| 77 | m.placing = make(map[string]int) |
| 78 | } |
| 79 | m.placing[dir]++ |
| 80 | m.touchLocked(dir) |
| 81 | m.placeMu.Unlock() |
| 82 | var once sync.Once |
| 83 | return func() { |
| 84 | once.Do(func() { |
| 85 | m.placeMu.Lock() |
| 86 | defer m.placeMu.Unlock() |
| 87 | if m.placing[dir]--; m.placing[dir] <= 0 { |
| 88 | delete(m.placing, dir) |
| 89 | } |
| 90 | m.touchLocked(dir) |
| 91 | // Wake every waiter; each rechecks its own subtree. |
| 92 | m.broadcastLocked() |
| 93 | }) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | // readQuiet runs read, a read of the kernel's view of dir, and reports |
| 98 | // whether no placement into dir or below it was in flight at any moment |
| 99 | // while read ran. Only a quiet read covers every process bound for dir: a |
| 100 | // placement that ended before read began is in the kernel's view, and one |
| 101 | // that registers after read ended had not forked while it ran. A nil |
| 102 | // manager has no placements: always quiet. |
| 103 | func (m *Manager) readQuiet(dir string, read func()) (quiet bool) { |
| 104 | dir = filepath.Clean(dir) |
| 105 | if m == nil { |
| 106 | read() |
| 107 | return true |
| 108 | } |
| 109 | w := &placeWindow{dir: dir} |
| 110 | m.placeMu.Lock() |
| 111 | w.busy = m.countUnderLocked(dir) > 0 |
| 112 | if m.windows == nil { |
| 113 | m.windows = make(map[*placeWindow]struct{}) |
| 114 | } |
| 115 | m.windows[w] = struct{}{} |
| 116 | m.placeMu.Unlock() |
| 117 | read() |
| 118 | if placementReadHook != nil { |
| 119 | placementReadHook(dir) |
| 120 | } |
| 121 | m.placeMu.Lock() |
| 122 | defer m.placeMu.Unlock() |
| 123 | delete(m.windows, w) |
| 124 | return !w.busy && !w.changed && m.countUnderLocked(dir) == 0 |
| 125 | } |
| 126 | |
| 127 | // holdPlacements keeps every placement into dir or below it from |
| 128 | // registering until release is called, and answers the placements already |
| 129 | // in flight: at, into dir itself, and under, into dir or below it. With |
| 130 | // the hold in place those counts can only fall, and a count of zero stays |
| 131 | // zero, so a kernel read taken under the hold, with both zero, sees every |
| 132 | // process bound for dir. release is idempotent. Hold it only across |
| 133 | // cgroupfs reads and writes. |
| 134 | func (m *Manager) holdPlacements(dir string) (at, under int, release func()) { |
| 135 | if m == nil { |
| 136 | return 0, 0, func() {} |
| 137 | } |
| 138 | dir = filepath.Clean(dir) |
| 139 | m.placeMu.Lock() |
| 140 | if m.holds == nil { |
| 141 | m.holds = make(map[string]int) |
| 142 | } |
| 143 | m.holds[dir]++ |
| 144 | at, under = m.placing[dir], m.countUnderLocked(dir) |
| 145 | m.placeMu.Unlock() |
| 146 | var once sync.Once |
| 147 | return at, under, func() { |
| 148 | once.Do(func() { |
| 149 | m.placeMu.Lock() |
| 150 | defer m.placeMu.Unlock() |
| 151 | if m.holds[dir]--; m.holds[dir] <= 0 { |
| 152 | delete(m.holds, dir) |
| 153 | } |
| 154 | m.broadcastLocked() |
| 155 | }) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | // placementsUnder counts placements into dir or anywhere below it. |
| 160 | func (m *Manager) placementsUnder(dir string) int { |
| 161 | if m == nil { |
| 162 | return 0 |
| 163 | } |
| 164 | m.placeMu.Lock() |
| 165 | defer m.placeMu.Unlock() |
| 166 | return m.countUnderLocked(filepath.Clean(dir)) |
| 167 | } |
| 168 | |
| 169 | // placementsAt counts placements into dir itself. |
| 170 | func (m *Manager) placementsAt(dir string) int { |
| 171 | if m == nil { |
| 172 | return 0 |
| 173 | } |
| 174 | m.placeMu.Lock() |
| 175 | defer m.placeMu.Unlock() |
| 176 | return m.placing[filepath.Clean(dir)] |
| 177 | } |
| 178 | |
| 179 | // waitPlacementsUnder returns nil once no placement is in flight in dir or |
| 180 | // below it, or ctx's error if ctx ends first. |
| 181 | func (m *Manager) waitPlacementsUnder(ctx context.Context, dir string) error { |
| 182 | if m == nil { |
| 183 | return nil |
| 184 | } |
| 185 | dir = filepath.Clean(dir) |
| 186 | for { |
| 187 | m.placeMu.Lock() |
| 188 | if m.countUnderLocked(dir) == 0 { |
| 189 | m.placeMu.Unlock() |
| 190 | return nil |
| 191 | } |
| 192 | wake := m.wakeLocked() |
| 193 | m.placeMu.Unlock() |
| 194 | select { |
| 195 | case <-ctx.Done(): |
| 196 | return ctx.Err() |
| 197 | case <-wake: |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | // wakeLocked is the channel the next broadcastLocked closes. |
| 203 | func (m *Manager) wakeLocked() chan struct{} { |
| 204 | if m.placeWake == nil { |
| 205 | m.placeWake = make(chan struct{}) |
| 206 | } |
| 207 | return m.placeWake |
| 208 | } |
| 209 | |
| 210 | // broadcastLocked wakes every waiter: a registration ended, or a hold did. |
| 211 | func (m *Manager) broadcastLocked() { |
| 212 | if m.placeWake != nil { |
| 213 | close(m.placeWake) |
| 214 | m.placeWake = nil |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // touchLocked marks every open readQuiet over target as changed. |
| 219 | func (m *Manager) touchLocked(target string) { |
| 220 | for w := range m.windows { |
| 221 | if isUnder(target, w.dir) { |
| 222 | w.changed = true |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // heldLocked reports whether a hold covers target. |
| 228 | func (m *Manager) heldLocked(target string) bool { |
| 229 | for h := range m.holds { |
| 230 | if isUnder(target, h) { |
| 231 | return true |
| 232 | } |
| 233 | } |
| 234 | return false |
| 235 | } |
| 236 | |
| 237 | func (m *Manager) countUnderLocked(dir string) int { |
| 238 | n := 0 |
| 239 | for target, c := range m.placing { |
| 240 | if isUnder(target, dir) { |
| 241 | n += c |
| 242 | } |
| 243 | } |
| 244 | return n |
| 245 | } |
| 246 | |
| 247 | // isUnder reports whether target is dir or below it. Both are clean. |
| 248 | func isUnder(target, dir string) bool { |
| 249 | prefix := dir |
| 250 | if !strings.HasSuffix(prefix, "/") { |
| 251 | prefix += "/" |
| 252 | } |
| 253 | return target == dir || strings.HasPrefix(target, prefix) |
| 254 | } |
| 255 | |