| index cd4aa9b93..c6b381240 100644 |
| --- a/internal/activator/backend_exec.go |
| +++ b/internal/activator/backend_exec.go |
| @@ -364,7 +364,7 @@ func (b *ExecBackend) tier(rt registry.Route) cgroup.Limit { |
| // for the host in between. With Strict nil (a box with no moves wiring) |
| // none of this runs and no lock is taken: the spawn is main's exactly. |
| func (b *ExecBackend) spawnInCgroup(rt registry.Route, build func() *exec.Cmd) (*exec.Cmd, func(), error) { |
| - return b.spawnInCgroupGated(rt, build, nil) |
| + return b.spawnInCgroupGated(rt, cgroup.Limit{}, build, nil) |
| } |
| |
| // spawnInCgroupGated is spawnInCgroup with a before-start hook (final review |
| @@ -372,9 +372,16 @@ func (b *ExecBackend) spawnInCgroup(rt registry.Route, build func() *exec.Cmd) ( |
| // (StartGated), or, strict, after the strict group is prepared. A refusal |
| // starts nothing and is cleaned up as a failed start is. A nil before is |
| // spawnInCgroup exactly. |
| -func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) { |
| +// |
| +// tier is the artifact's own (a recorded or staged build's, ruling |
| +// R-TIER); zero is b.tier(rt), the live link's, which is every caller but |
| +// a staged spawn. |
| +func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, tier cgroup.Limit, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) { |
| + if tier == (cgroup.Limit{}) { |
| + tier = b.tier(rt) |
| + } |
| if b.Strict == nil { |
| - cmd, err := b.Cgroups.StartGated(rt.Host, b.tier(rt), build, before) |
| + cmd, err := b.Cgroups.StartGated(rt.Host, tier, build, before) |
| return cmd, func() {}, err |
| } |
| strict := b.Strict(rt.Host) |
| @@ -393,14 +400,14 @@ func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, build func() *exec.C |
| return nil, nil, fmt.Errorf("%s: %w", rt.Host, cause) |
| } |
| if !strict { |
| - cmd, err := b.Cgroups.StartGated(rt.Host, b.tier(rt), build, before) |
| + cmd, err := b.Cgroups.StartGated(rt.Host, tier, build, before) |
| g.mu.Unlock() |
| return cmd, func() {}, err |
| } |
| o := &strictOwner{} |
| g.owner = o |
| g.mu.Unlock() |
| - sp, err := b.prepareStrictProc(rt.Host, b.tier(rt)) |
| + sp, err := b.prepareStrictProc(rt.Host, tier) |
| if err != nil { |
| b.disown(rt.Host, o, false) |
| return nil, nil, fmt.Errorf("%s: %w: %v", rt.Host, ErrStrictRequired, err) |
| @@ -780,30 +787,33 @@ func (b *ExecBackend) Seed(ctx context.Context, rt registry.Route) error { |
| p := b.pathsFor(rt) |
| switch p.layout { |
| case LayoutNested: |
| - return b.seedUnit(ctx, rt, nil) |
| + return b.seedUnit(ctx, rt, nil, cgroup.Limit{}) |
| case LayoutFenced, LayoutConflict: |
| return fmt.Errorf("%s: refusing to seed: %s", rt.Host, p.reason) |
| } |
| - return b.seedChild(ctx, rt, b.liveLink(rt.Host)) |
| + return b.seedChild(ctx, rt, b.liveLink(rt.Host), cgroup.Limit{}) |
| } |
| |
| // SeedExe is Seed on the identity a wake recorded (settings spec §4.2): |
| // id.StagedPath is checked against id.Digest first, and a copy that |
| // changed seeds nothing (never the live link instead). A flat or foreign |
| // row execs that file by path. A nested row asks root to run the seed unit |
| -// on it, which root validates and binds in place of the live link. |
| +// on it, which root validates and binds in place of the live link. The |
| +// seed runs at id's own tier (GenTier, ruling R-TIER), never the tier of |
| +// whatever the live link names by then. |
| func (b *ExecBackend) SeedExe(ctx context.Context, rt registry.Route, id release.StagedIdentity) error { |
| if err := release.VerifyStaged(id); err != nil { |
| return fmt.Errorf("%s: not seeded with %s: %w", rt.Host, id.Version, err) |
| } |
| + tier := b.GenTier(rt, id.Version) |
| p := b.pathsFor(rt) |
| switch p.layout { |
| case LayoutNested: |
| - return b.seedUnit(ctx, rt, stagedExeOf(id)) |
| + return b.seedUnit(ctx, rt, stagedExeOf(id), tier) |
| case LayoutFenced, LayoutConflict: |
| return fmt.Errorf("%s: refusing to seed: %s", rt.Host, p.reason) |
| } |
| - return b.seedChild(ctx, rt, id.StagedPath) |
| + return b.seedChild(ctx, rt, id.StagedPath, tier) |
| } |
| |
| // seedArgv parses rt.Seed's JSON argv array and appends the --db flag, the |
| @@ -837,7 +847,8 @@ func removeSeedDB(dbPath string) { |
| // seedChild is Seed's flat body. exe is the file the seed execs: the live |
| // link (Seed), which is materialized first if it is missing, or a staged |
| // path SeedExe verified, which is exec'd by path whatever the link names. |
| -func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe string) error { |
| +// tier is that file's own; zero is the live tier (b.tier). |
| +func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe string, tier cgroup.Limit) error { |
| host := rt.Host |
| paths := b.pathsFor(rt) |
| argv, err := seedArgv(rt, paths.dbPath) |
| @@ -883,7 +894,7 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri |
| if perr != nil { |
| return perr |
| } |
| - cmd, settle, err := b.spawnInCgroupAt(rt, child, false, func() *exec.Cmd { |
| + cmd, settle, err := b.spawnInCgroupAtTier(rt, child, false, tier, func() *exec.Cmd { |
| cmd := exec.CommandContext(ctx, exe, argv...) |
| // The control-channel vars are agent-owned in BOTH directions here too |
| // (Start's discipline, see withoutVars), and here they are always |
| @@ -916,7 +927,7 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri |
| // wedged seed would hold the lease until the wake's own 5m budget expired. |
| cmd.WaitDelay = seedDrainGrace |
| return cmd |
| - }) |
| + }, nil) |
| if err == nil { |
| err = cmd.Wait() |
| settle() |
| @@ -1017,11 +1028,15 @@ func (c *capBuffer) String() string { |
| // wake recorded (SeedExe): root validates it and binds the seed unit's app |
| // mount to it, so the live link need not exist and a repoint of it |
| // changes nothing. A control channel that cannot name it refuses the seed. |
| -func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe) error { |
| +// tier is that build's own (ruling R-TIER); zero is the live tier. |
| +func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe, tier cgroup.Limit) error { |
| host := rt.Host |
| if err := b.nestedGate(rt); err != nil { |
| return err |
| } |
| + if tier == (cgroup.Limit{}) { |
| + tier = b.tier(rt) |
| + } |
| var xc StagedExeControl |
| if exe == nil { |
| if err := b.ensureLiveLink(ctx, host); err != nil { |
| @@ -1067,9 +1082,9 @@ func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *inst |
| sctx, cancel := context.WithTimeout(ctx, instancectl.DeadlineSeed) |
| var res instancectl.SeedResponse |
| if exe != nil { |
| - res, err = xc.SeedExe(sctx, host, b.tier(rt).MemoryMax, *exe) |
| + res, err = xc.SeedExe(sctx, host, tier.MemoryMax, *exe) |
| } else { |
| - res, err = b.Control.Seed(sctx, host, b.tier(rt).MemoryMax) |
| + res, err = b.Control.Seed(sctx, host, tier.MemoryMax) |
| } |
| cancel() |
| if errors.Is(err, instancectl.ErrStagedExe) { |
| @@ -1266,11 +1281,11 @@ func (b *ExecBackend) Start(ctx context.Context, rt registry.Route) (Proc, strin |
| p := b.pathsFor(rt) |
| switch p.layout { |
| case LayoutNested: |
| - return b.startUnitAt(ctx, rt, rt.Socket, 0, nil) |
| + return b.startUnitAt(ctx, rt, rt.Socket, 0, nil, cgroup.Limit{}) |
| case LayoutFenced, LayoutConflict: |
| return nil, "", fmt.Errorf("%s: refusing to wake: %s", rt.Host, p.reason) |
| } |
| - return b.startChildAt(ctx, rt, rt.Socket, 0, "") |
| + return b.startChildAt(ctx, rt, rt.Socket, 0, "", cgroup.Limit{}) |
| } |
| |
| // StartGen is Start for generation n (zero-downtime spec §7.1). perGen false |
| @@ -1286,7 +1301,7 @@ func (b *ExecBackend) Start(ctx context.Context, rt registry.Route) (Proc, strin |
| // whose counter started at 1 like this one's). A sibling generation's path |
| // is never touched. |
| func (b *ExecBackend) StartGen(ctx context.Context, rt registry.Route, n uint64, perGen bool) (Proc, Gen, error) { |
| - return b.startGenFrom(ctx, rt, n, perGen, nil) |
| + return b.startGenFrom(ctx, rt, n, perGen, nil, cgroup.Limit{}) |
| } |
| |
| // StartGenExe is StartGen on the identity a wake recorded (settings spec |
| @@ -1296,22 +1311,30 @@ func (b *ExecBackend) StartGen(ctx context.Context, rt registry.Route, n uint64, |
| // foreign row execs the file by path. A nested row asks root to bind it in |
| // place of the live link: the original run unit while GenContainment is |
| // off (generation 0), generation n's unit while it is on. The handle names |
| -// id's build, path, digest and demand, so a sidecar can bind the same file. |
| +// id's build, path, digest, demand and tier, so a sidecar can bind the same |
| +// file. |
| +// |
| +// The tier is id's own (GenTier, as StartStaged takes it; ruling R-TIER), |
| +// never b.tier(rt): that reads whatever the live link names when it is |
| +// asked, and root may have repointed the link since the record, which |
| +// would run A's bytes under B's limits. |
| func (b *ExecBackend) StartGenExe(ctx context.Context, rt registry.Route, n uint64, perGen bool, id release.StagedIdentity) (Proc, Gen, error) { |
| if err := release.VerifyStaged(id); err != nil { |
| return nil, Gen{}, fmt.Errorf("%s: not started on %s: %w", rt.Host, id.Version, err) |
| } |
| - proc, g, err := b.startGenFrom(ctx, rt, n, perGen, &id) |
| + tier := b.GenTier(rt, id.Version) |
| + proc, g, err := b.startGenFrom(ctx, rt, n, perGen, &id, tier) |
| if err != nil { |
| return proc, g, err |
| } |
| - g.Build, g.StagedPath, g.Digest, g.Demand = id.Version, id.StagedPath, id.Digest, id.Demand |
| + g.Build, g.StagedPath, g.Digest, g.Demand, g.Tier = id.Version, id.StagedPath, id.Digest, id.Demand, tier |
| return proc, g, nil |
| } |
| |
| // startGenFrom is StartGen's body. A nil id is the live link (StartGen); a |
| -// non-nil one is the verified file StartGenExe names. |
| -func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uint64, perGen bool, id *release.StagedIdentity) (Proc, Gen, error) { |
| +// non-nil one is the verified file StartGenExe names. tier is that file's |
| +// own; zero is the live tier (b.tier). |
| +func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uint64, perGen bool, id *release.StagedIdentity, tier cgroup.Limit) (Proc, Gen, error) { |
| var ( |
| flatExe string |
| unitExe *instancectl.StagedExe |
| @@ -1337,7 +1360,7 @@ func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uin |
| switch p.layout { |
| case LayoutNested: |
| // prepareAt removes the stale file at sock for the nested layout. |
| - proc, ver, err = b.startUnitAt(ctx, rt, sock, n, unitExe) |
| + proc, ver, err = b.startUnitAt(ctx, rt, sock, n, unitExe, tier) |
| case LayoutFenced, LayoutConflict: |
| return nil, Gen{}, fmt.Errorf("%s: refusing to wake: %s", rt.Host, p.reason) |
| default: |
| @@ -1353,7 +1376,7 @@ func (b *ExecBackend) startGenFrom(ctx context.Context, rt registry.Route, n uin |
| return nil, Gen{}, fmt.Errorf("%s: generation socket: %w", rt.Host, err) |
| } |
| } |
| - proc, ver, err = b.startChildAt(ctx, rt, sock, n, flatExe) |
| + proc, ver, err = b.startChildAt(ctx, rt, sock, n, flatExe, tier) |
| } |
| g := Gen{N: n, Socket: sock, Control: sock + ".control"} |
| if err != nil { |
| @@ -1399,8 +1422,9 @@ func clearOwnGenSocket(sock string) error { |
| // (StartGenExe). spawnChildFrom leaves a staged spawn's cache purge to the |
| // swap's switch, but a wake's staged spawn is the host's only process, so |
| // its purge happens here, at the same moment, before the health wait. |
| -func (b *ExecBackend) startChildAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe string) (Proc, string, error) { |
| - p, spawnVer, err := b.spawnChildFrom(ctx, rt, sock, n, exe, cgroup.Limit{}) |
| +// tier is exe's own; zero is the live tier (b.tier). |
| +func (b *ExecBackend) startChildAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe string, tier cgroup.Limit) (Proc, string, error) { |
| + p, spawnVer, err := b.spawnChildFrom(ctx, rt, sock, n, exe, tier) |
| if err != nil { |
| return p, "", err |
| } |
| @@ -1797,8 +1821,18 @@ func (b *ExecBackend) spawnUnitFrom(ctx context.Context, rt registry.Route, sock |
| inv, err := b.startRole(ctx, rt, instancectl.RoleRun, gen, exe, tier) |
| // Past this call root may have started the unit: every failure below |
| // but ErrStagedExe (root's refusal before it laid out or started |
| - // anything) is marked startUncertain, since the build may have run |
| - // before it failed or exited (final review wave 2, N4). |
| + // anything) and errNoStagedExeControl (root was never asked: the |
| + // channel cannot name the file) is marked startUncertain, since the |
| + // build may have run before it failed or exited (final review wave 2, |
| + // N4). |
| + if errors.Is(err, errNoStagedExeControl) { |
| + // Nothing was sent to root. Proven not run, so never a failure |
| + // record. A generation's own launch file goes as after a refusal. |
| + if gen != 0 { |
| + _ = os.Remove(filepath.Join(p.dataDir, instancectl.GenLaunchFileName(gen))) |
| + } |
| + return fail(fmt.Errorf("%s: start unit: %w", rt.Host, err)) |
| + } |
| if errors.Is(err, instancectl.ErrStagedExe) { |
| // Root refused the staged build before it laid out or started |
| // anything, so there is nothing to kill and nothing unproven. The |
| @@ -1853,9 +1887,10 @@ func (b *ExecBackend) spawnUnitFrom(ctx context.Context, rt registry.Route, sock |
| // than exec'd directly. The agent has no privilege to enter the tenant's |
| // namespace, only the control service does. It is spawnUnitFrom, then the |
| // health wait. exe is nil for the live link, or the staged file a wake |
| -// recorded (StartGenExe), which root binds in place of the link. |
| -func (b *ExecBackend) startUnitAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe *instancectl.StagedExe) (Proc, string, error) { |
| - p, spawnVer, err := b.spawnUnitFrom(ctx, rt, sock, n, exe, cgroup.Limit{}) |
| +// recorded (StartGenExe), which root binds in place of the link. tier is |
| +// exe's own; zero is the live tier (b.tier). |
| +func (b *ExecBackend) startUnitAt(ctx context.Context, rt registry.Route, sock string, n uint64, exe *instancectl.StagedExe, tier cgroup.Limit) (Proc, string, error) { |
| + p, spawnVer, err := b.spawnUnitFrom(ctx, rt, sock, n, exe, tier) |
| if err != nil { |
| return p, "", err |
| } |