| 1 | //go:build linux |
| 2 | |
| 3 | package cgroup |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "os" |
| 9 | "os/exec" |
| 10 | "slices" |
| 11 | "strconv" |
| 12 | "strings" |
| 13 | |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | // tierExpected is the four limit files for l, as the kernel reads them |
| 18 | // back: memory.high rounded down to the page. |
| 19 | func tierExpected(l Limit, oomGroup string) map[string]string { |
| 20 | page := int64(os.Getpagesize()) |
| 21 | return map[string]string{ |
| 22 | "memory.max": strconv.FormatInt(l.MemoryMax, 10), |
| 23 | "memory.high": strconv.FormatInt(HighOf(l.MemoryMax)/page*page, 10), |
| 24 | "memory.swap.max": strconv.FormatInt(l.MemoryMax, 10), |
| 25 | "memory.oom.group": oomGroup, |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // pinChild pins a child cgroup under parent: beforeMkdir callback (if provided), |
| 30 | // mkdir, create child struct, FcntlInt, strictOpen (dir, events, kill), strictID, |
| 31 | // reservation, and population check. |
| 32 | // beforeMkdir is called before mkdir (for role intent persistence; may be nil). |
| 33 | // allowExisting controls whether mkdir's EEXIST is accepted: true allows reusing |
| 34 | // an existing directory (generation children, role children without intent), |
| 35 | // false refuses it (role children with intent require a new name). |
| 36 | // On success, returns a fully pinned, reserved child. |
| 37 | // On error, all cleanup is done internally; caller needs only to set up a defer |
| 38 | // for cleanup of any subsequent operations (e.g. tier writes for generation children). |
| 39 | func pinChild(parent *Prepared, name string, beforeMkdir func() error, allowExisting bool) (*Prepared, error) { |
| 40 | if beforeMkdir != nil { |
| 41 | if err := beforeMkdir(); err != nil { |
| 42 | return nil, err |
| 43 | } |
| 44 | } |
| 45 | if err := unix.Mkdirat(int(parent.dir.Fd()), name, 0755); err != nil { |
| 46 | if !allowExisting || !errors.Is(err, unix.EEXIST) { |
| 47 | return nil, ErrStrictRefused |
| 48 | } |
| 49 | // An existing child that took cgroup.kill is never reused (amadan |
| 50 | // #272). The caller holds the parent's mu. |
| 51 | if err := refuseKilledChild(parent, name); err != nil { |
| 52 | return nil, err |
| 53 | } |
| 54 | } |
| 55 | child := &Prepared{strictPreparedState: &strictPreparedState{parent: parent, host: parent.host, limits: map[string]*os.File{}, expected: map[string]string{}, entryName: name, start: func(cmd *exec.Cmd) error { return cmd.Start() }}} |
| 56 | |
| 57 | fd, e := unix.FcntlInt(parent.dir.Fd(), unix.F_DUPFD_CLOEXEC, 0) |
| 58 | if e != nil { |
| 59 | return nil, ErrStrictUnsupported |
| 60 | } |
| 61 | child.entryParent = os.NewFile(uintptr(fd), name+"-parent") |
| 62 | |
| 63 | var err error |
| 64 | child.dir, err = strictOpen(parent.dir, name, unix.O_RDONLY|unix.O_DIRECTORY) |
| 65 | if err != nil { |
| 66 | child.closeFiles() |
| 67 | return nil, ErrStrictRefused |
| 68 | } |
| 69 | |
| 70 | child.identity, err = strictID(child.dir) |
| 71 | if err != nil { |
| 72 | child.closeFiles() |
| 73 | return nil, ErrStrictRefused |
| 74 | } |
| 75 | |
| 76 | strictReservations.Lock() |
| 77 | if strictReservations.held[child.identity] { |
| 78 | strictReservations.Unlock() |
| 79 | child.closeFiles() |
| 80 | return nil, ErrStrictState |
| 81 | } |
| 82 | strictReservations.held[child.identity] = true |
| 83 | strictReservations.Unlock() |
| 84 | |
| 85 | child.events, err = strictOpen(child.dir, "cgroup.events", unix.O_RDONLY) |
| 86 | if err != nil { |
| 87 | child.closeFiles() |
| 88 | strictReservations.Lock() |
| 89 | delete(strictReservations.held, child.identity) |
| 90 | strictReservations.Unlock() |
| 91 | return nil, ErrStrictUnsupported |
| 92 | } |
| 93 | |
| 94 | child.kill, err = strictOpen(child.dir, "cgroup.kill", unix.O_WRONLY) |
| 95 | if err != nil { |
| 96 | child.closeFiles() |
| 97 | strictReservations.Lock() |
| 98 | delete(strictReservations.held, child.identity) |
| 99 | strictReservations.Unlock() |
| 100 | return nil, ErrStrictUnsupported |
| 101 | } |
| 102 | |
| 103 | populated, err := strictPopulation(child.events) |
| 104 | if err != nil || populated { |
| 105 | child.closeFiles() |
| 106 | strictReservations.Lock() |
| 107 | delete(strictReservations.held, child.identity) |
| 108 | strictReservations.Unlock() |
| 109 | return nil, ErrStrictRefused |
| 110 | } |
| 111 | |
| 112 | return child, nil |
| 113 | } |
| 114 | |
| 115 | // delegateStrict delegates a generation parent's controllers to its |
| 116 | // children and proves memory is among them: a generation child without it |
| 117 | // could carry no limit. |
| 118 | func delegateStrict(dir *os.File) error { |
| 119 | ctl, e := strictOpen(dir, "cgroup.controllers", unix.O_RDONLY) |
| 120 | if e != nil { |
| 121 | return ErrStrictUnsupported |
| 122 | } |
| 123 | have, e := strictRead(ctl) |
| 124 | ctl.Close() |
| 125 | if e != nil { |
| 126 | return ErrStrictRefused |
| 127 | } |
| 128 | want := delegatedControllers(have) |
| 129 | if !strings.Contains(want, "+memory") { |
| 130 | return ErrStrictUnsupported |
| 131 | } |
| 132 | sub, e := strictOpen(dir, "cgroup.subtree_control", unix.O_RDWR) |
| 133 | if e != nil { |
| 134 | return ErrStrictUnsupported |
| 135 | } |
| 136 | defer sub.Close() |
| 137 | if e = strictWrite(sub, want); e != nil { |
| 138 | return ErrStrictRefused |
| 139 | } |
| 140 | got, e := strictRead(sub) |
| 141 | if e != nil || !slices.Contains(strings.Fields(got), "memory") { |
| 142 | return ErrStrictRefused |
| 143 | } |
| 144 | return nil |
| 145 | } |
| 146 | |
| 147 | // PrepareStrictParent pins host's cgroup as a generation parent for the life |
| 148 | // of one activator entry (zero-downtime deploys spec §7.3): empty, the tier |
| 149 | // written with memory.oom.group=0, its controllers delegated to its |
| 150 | // children. It holds no process: Start refuses it. Each spawn pins and |
| 151 | // reserves its own child with PrepareGen. The swap moves the parent's limit |
| 152 | // with SetParentLimit, which keeps the expected-limit record in step. |
| 153 | func (m *Manager) PrepareStrictParent(host string, tier Limit) (*Prepared, error) { |
| 154 | return m.prepareStrict(host, tier, true) |
| 155 | } |
| 156 | |
| 157 | // PrepareGen pins generation n's child under a generation parent: newly |
| 158 | // empty, reserved for this generation alone, with the tier's full limits |
| 159 | // and memory.oom.group=1 (spec §7.3). tier is this generation's own and may |
| 160 | // differ from the incumbent's when a release changes tier. |
| 161 | func (p *Prepared) PrepareGen(n uint64, tier Limit) (out *Prepared, err error) { |
| 162 | if p == nil || p.strictPreparedState == nil || n == 0 || (tier != TierDefault && tier != TierChildren) { |
| 163 | return nil, ErrStrictRefused |
| 164 | } |
| 165 | p.mu.Lock() |
| 166 | defer p.mu.Unlock() |
| 167 | if !p.genParent || p.closed || p.empty || p.draining.Load() { |
| 168 | return nil, ErrStrictState |
| 169 | } |
| 170 | if err = p.identityOK(); err != nil { |
| 171 | return nil, err |
| 172 | } |
| 173 | name := GenChild(n) |
| 174 | child, err := pinChild(p, name, nil, true) |
| 175 | if err != nil { |
| 176 | return nil, err |
| 177 | } |
| 178 | defer func() { |
| 179 | if err != nil { |
| 180 | child.closeFiles() |
| 181 | strictReservations.Lock() |
| 182 | delete(strictReservations.held, child.identity) |
| 183 | strictReservations.Unlock() |
| 184 | } |
| 185 | }() |
| 186 | child.tier = tier |
| 187 | want := tierExpected(tier, "1") |
| 188 | var e error |
| 189 | for name := range want { |
| 190 | if child.limits[name], e = strictOpen(child.dir, name, unix.O_RDWR); e != nil { |
| 191 | return nil, ErrStrictUnsupported |
| 192 | } |
| 193 | } |
| 194 | for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} { |
| 195 | if e = strictWrite(child.limits[name], want[name]); e != nil { |
| 196 | return nil, ErrStrictRefused |
| 197 | } |
| 198 | child.expected[name] = want[name] |
| 199 | } |
| 200 | if e = child.limitsOK(); e != nil { |
| 201 | return nil, e |
| 202 | } |
| 203 | if e = child.identityOK(); e != nil { |
| 204 | return nil, e |
| 205 | } |
| 206 | return child, nil |
| 207 | } |
| 208 | |
| 209 | // SetParentLimit writes l onto a generation parent and records it as the |
| 210 | // expected value in the same step (spec §7.3). A child's Start checks the |
| 211 | // parent with limitsOK and must never see a limit this API did not set. |
| 212 | // Each file's record moves only after its own write lands, so a partial |
| 213 | // failure leaves the record matching the kernel. A limit someone else set |
| 214 | // is refused, not overwritten. |
| 215 | func (p *Prepared) SetParentLimit(l Limit) error { |
| 216 | if p == nil || p.strictPreparedState == nil || !ValidParentLimit(l) { |
| 217 | return ErrStrictRefused |
| 218 | } |
| 219 | p.mu.Lock() |
| 220 | defer p.mu.Unlock() |
| 221 | if !p.genParent || p.closed || p.empty || p.draining.Load() { |
| 222 | return ErrStrictState |
| 223 | } |
| 224 | if e := p.identityOK(); e != nil { |
| 225 | return e |
| 226 | } |
| 227 | if e := p.limitsOK(); e != nil { |
| 228 | return e |
| 229 | } |
| 230 | want := tierExpected(l, "0") |
| 231 | for _, name := range []string{"memory.max", "memory.high", "memory.swap.max"} { |
| 232 | if e := strictWrite(p.limits[name], want[name]); e != nil { |
| 233 | return ErrStrictRefused |
| 234 | } |
| 235 | p.expected[name] = want[name] |
| 236 | } |
| 237 | return p.limitsOK() |
| 238 | } |
| 239 | |
| 240 | // CloseRemove is Close for a generation child, then removes the child's |
| 241 | // directory through the parent descriptor pinned at PrepareGen. Generation |
| 242 | // numbers are never reused, so a child left behind would stay until the |
| 243 | // agent restarts. Only an empty child is closed. The handle is closed and |
| 244 | // its reservation released even when the rmdir fails; that error is |
| 245 | // returned, and the next agent start's sweep removes the directory. |
| 246 | func (p *Prepared) CloseRemove() error { |
| 247 | if p == nil || p.strictPreparedState == nil { |
| 248 | return ErrStrictState |
| 249 | } |
| 250 | p.mu.Lock() |
| 251 | defer p.mu.Unlock() |
| 252 | p.draining.Store(true) |
| 253 | if p.closed || p.parent == nil || p.entryParent == nil || !isGenName(p.entryName) { |
| 254 | return ErrStrictState |
| 255 | } |
| 256 | if e := p.identityOK(); e != nil { |
| 257 | return e |
| 258 | } |
| 259 | if populated, e := strictPopulation(p.events); e != nil || populated { |
| 260 | return ErrStrictRefused |
| 261 | } |
| 262 | rmErr := unix.Unlinkat(int(p.entryParent.Fd()), p.entryName, unix.AT_REMOVEDIR) |
| 263 | p.closed = true |
| 264 | p.closeFiles() |
| 265 | strictReservations.Lock() |
| 266 | delete(strictReservations.held, p.identity) |
| 267 | strictReservations.Unlock() |
| 268 | if rmErr != nil { |
| 269 | return fmt.Errorf("cgroup: %s: remove %s: %w", p.host, p.entryName, rmErr) |
| 270 | } |
| 271 | return nil |
| 272 | } |
| 273 | |