//go:build linux package cgroup import ( "errors" "fmt" "os" "os/exec" "slices" "strconv" "strings" "golang.org/x/sys/unix" ) // tierExpected is the four limit files for l, as the kernel reads them // back: memory.high rounded down to the page. func tierExpected(l Limit, oomGroup string) map[string]string { page := int64(os.Getpagesize()) return map[string]string{ "memory.max": strconv.FormatInt(l.MemoryMax, 10), "memory.high": strconv.FormatInt(HighOf(l.MemoryMax)/page*page, 10), "memory.swap.max": strconv.FormatInt(l.MemoryMax, 10), "memory.oom.group": oomGroup, } } // pinChild pins a child cgroup under parent: beforeMkdir callback (if provided), // mkdir, create child struct, FcntlInt, strictOpen (dir, events, kill), strictID, // reservation, and population check. // beforeMkdir is called before mkdir (for role intent persistence; may be nil). // allowExisting controls whether mkdir's EEXIST is accepted: true allows reusing // an existing directory (generation children, role children without intent), // false refuses it (role children with intent require a new name). // On success, returns a fully pinned, reserved child. // On error, all cleanup is done internally; caller needs only to set up a defer // for cleanup of any subsequent operations (e.g. tier writes for generation children). func pinChild(parent *Prepared, name string, beforeMkdir func() error, allowExisting bool) (*Prepared, error) { if beforeMkdir != nil { if err := beforeMkdir(); err != nil { return nil, err } } if err := unix.Mkdirat(int(parent.dir.Fd()), name, 0755); err != nil { if !allowExisting || !errors.Is(err, unix.EEXIST) { return nil, ErrStrictRefused } // An existing child that took cgroup.kill is never reused (amadan // #272). The caller holds the parent's mu. if err := refuseKilledChild(parent, name); err != nil { return nil, err } } 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() }}} fd, e := unix.FcntlInt(parent.dir.Fd(), unix.F_DUPFD_CLOEXEC, 0) if e != nil { return nil, ErrStrictUnsupported } child.entryParent = os.NewFile(uintptr(fd), name+"-parent") var err error child.dir, err = strictOpen(parent.dir, name, unix.O_RDONLY|unix.O_DIRECTORY) if err != nil { child.closeFiles() return nil, ErrStrictRefused } child.identity, err = strictID(child.dir) if err != nil { child.closeFiles() return nil, ErrStrictRefused } strictReservations.Lock() if strictReservations.held[child.identity] { strictReservations.Unlock() child.closeFiles() return nil, ErrStrictState } strictReservations.held[child.identity] = true strictReservations.Unlock() child.events, err = strictOpen(child.dir, "cgroup.events", unix.O_RDONLY) if err != nil { child.closeFiles() strictReservations.Lock() delete(strictReservations.held, child.identity) strictReservations.Unlock() return nil, ErrStrictUnsupported } child.kill, err = strictOpen(child.dir, "cgroup.kill", unix.O_WRONLY) if err != nil { child.closeFiles() strictReservations.Lock() delete(strictReservations.held, child.identity) strictReservations.Unlock() return nil, ErrStrictUnsupported } populated, err := strictPopulation(child.events) if err != nil || populated { child.closeFiles() strictReservations.Lock() delete(strictReservations.held, child.identity) strictReservations.Unlock() return nil, ErrStrictRefused } return child, nil } // delegateStrict delegates a generation parent's controllers to its // children and proves memory is among them: a generation child without it // could carry no limit. func delegateStrict(dir *os.File) error { ctl, e := strictOpen(dir, "cgroup.controllers", unix.O_RDONLY) if e != nil { return ErrStrictUnsupported } have, e := strictRead(ctl) ctl.Close() if e != nil { return ErrStrictRefused } want := delegatedControllers(have) if !strings.Contains(want, "+memory") { return ErrStrictUnsupported } sub, e := strictOpen(dir, "cgroup.subtree_control", unix.O_RDWR) if e != nil { return ErrStrictUnsupported } defer sub.Close() if e = strictWrite(sub, want); e != nil { return ErrStrictRefused } got, e := strictRead(sub) if e != nil || !slices.Contains(strings.Fields(got), "memory") { return ErrStrictRefused } return nil } // PrepareStrictParent pins host's cgroup as a generation parent for the life // of one activator entry (zero-downtime deploys spec §7.3): empty, the tier // written with memory.oom.group=0, its controllers delegated to its // children. It holds no process: Start refuses it. Each spawn pins and // reserves its own child with PrepareGen. The swap moves the parent's limit // with SetParentLimit, which keeps the expected-limit record in step. func (m *Manager) PrepareStrictParent(host string, tier Limit) (*Prepared, error) { return m.prepareStrict(host, tier, true) } // PrepareGen pins generation n's child under a generation parent: newly // empty, reserved for this generation alone, with the tier's full limits // and memory.oom.group=1 (spec §7.3). tier is this generation's own and may // differ from the incumbent's when a release changes tier. func (p *Prepared) PrepareGen(n uint64, tier Limit) (out *Prepared, err error) { if p == nil || p.strictPreparedState == nil || n == 0 || (tier != TierDefault && tier != TierChildren) { return nil, ErrStrictRefused } p.mu.Lock() defer p.mu.Unlock() if !p.genParent || p.closed || p.empty || p.draining.Load() { return nil, ErrStrictState } if err = p.identityOK(); err != nil { return nil, err } name := GenChild(n) child, err := pinChild(p, name, nil, true) if err != nil { return nil, err } defer func() { if err != nil { child.closeFiles() strictReservations.Lock() delete(strictReservations.held, child.identity) strictReservations.Unlock() } }() child.tier = tier want := tierExpected(tier, "1") var e error for name := range want { if child.limits[name], e = strictOpen(child.dir, name, unix.O_RDWR); e != nil { return nil, ErrStrictUnsupported } } for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} { if e = strictWrite(child.limits[name], want[name]); e != nil { return nil, ErrStrictRefused } child.expected[name] = want[name] } if e = child.limitsOK(); e != nil { return nil, e } if e = child.identityOK(); e != nil { return nil, e } return child, nil } // SetParentLimit writes l onto a generation parent and records it as the // expected value in the same step (spec §7.3). A child's Start checks the // parent with limitsOK and must never see a limit this API did not set. // Each file's record moves only after its own write lands, so a partial // failure leaves the record matching the kernel. A limit someone else set // is refused, not overwritten. func (p *Prepared) SetParentLimit(l Limit) error { if p == nil || p.strictPreparedState == nil || !ValidParentLimit(l) { return ErrStrictRefused } p.mu.Lock() defer p.mu.Unlock() if !p.genParent || p.closed || p.empty || p.draining.Load() { return ErrStrictState } if e := p.identityOK(); e != nil { return e } if e := p.limitsOK(); e != nil { return e } want := tierExpected(l, "0") for _, name := range []string{"memory.max", "memory.high", "memory.swap.max"} { if e := strictWrite(p.limits[name], want[name]); e != nil { return ErrStrictRefused } p.expected[name] = want[name] } return p.limitsOK() } // CloseRemove is Close for a generation child, then removes the child's // directory through the parent descriptor pinned at PrepareGen. Generation // numbers are never reused, so a child left behind would stay until the // agent restarts. Only an empty child is closed. The handle is closed and // its reservation released even when the rmdir fails; that error is // returned, and the next agent start's sweep removes the directory. func (p *Prepared) CloseRemove() error { if p == nil || p.strictPreparedState == nil { return ErrStrictState } p.mu.Lock() defer p.mu.Unlock() p.draining.Store(true) if p.closed || p.parent == nil || p.entryParent == nil || !isGenName(p.entryName) { return ErrStrictState } if e := p.identityOK(); e != nil { return e } if populated, e := strictPopulation(p.events); e != nil || populated { return ErrStrictRefused } rmErr := unix.Unlinkat(int(p.entryParent.Fd()), p.entryName, unix.AT_REMOVEDIR) p.closed = true p.closeFiles() strictReservations.Lock() delete(strictReservations.held, p.identity) strictReservations.Unlock() if rmErr != nil { return fmt.Errorf("cgroup: %s: remove %s: %w", p.host, p.entryName, rmErr) } return nil }