//go:build linux package host import ( "context" "os" "path/filepath" "strconv" "strings" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" "golang.org/x/sys/unix" ) func generationIdentity(path string) (rootstate.FileIdentity, error) { var st unix.Stat_t if err := unix.Stat(path, &st); err != nil { return rootstate.FileIdentity{}, err } return rootstate.FileIdentity{Device: uint64(st.Dev), Inode: st.Ino}, nil } func (k generationKernel) containedTask(ctx context.Context, path string, status []byte) (bool, error) { raw, err := generationRead(path + "/cgroup") if err != nil { return false, err } group := strings.TrimSpace(string(raw)) for _, r := range k.contained { p := r.Process if p.Containment != 1 || r.Closed || r.Retirement != "" || group != "0::/"+p.Group { continue } if err := generationContainedStatus(status); err != nil { return false, err } if err := k.containedBinding(ctx, path, r); err != nil { return false, err } return true, nil } return false, nil } func generationContainedStatus(raw []byte) error { fields := map[string]string{} for _, line := range strings.Split(string(raw), "\n") { key, value, ok := strings.Cut(line, ":") if ok { if _, dup := fields[key]; dup { return os.ErrPermission } fields[key] = strings.TrimSpace(value) } } for _, key := range []string{"CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb"} { n, err := strconv.ParseUint(fields[key], 16, 64) if err != nil || n != 0 { return os.ErrPermission } } if fields["NoNewPrivs"] != "1" || fields["Seccomp"] != "2" { return os.ErrPermission } return nil } func (k generationKernel) containedBinding(ctx context.Context, path string, r release.GenerationReservation) error { if err := ctx.Err(); err != nil { return err } b, p := r.Binding, r.Process boot, err := k.boot() if err != nil || boot != b.Target.BootID || p.Group != generationBindingGroup(b) { return os.ErrPermission } var group unix.Stat_t if err := unix.Lstat(filepath.Join(k.cgroups, p.Group), &group); err != nil { return err } if group.Uid != k.owner || group.Mode&0022 != 0 || group.Mode&unix.S_IFMT != unix.S_IFDIR || (rootstate.FileIdentity{Device: uint64(group.Dev), Inode: group.Ino}) != p.GroupIdentity { return os.ErrPermission } main, tenant := filepath.Join(k.proc, strconv.Itoa(p.PID)), filepath.Join(k.proc, strconv.Itoa(p.TenantPID)) for pid, want := range map[int]string{p.PID: p.Start, p.TenantPID: p.TenantStart} { start, err := claimLaunchProcessStart(k.proc, pid) if err != nil || start != want { return os.ErrPermission } raw, err := generationRead(filepath.Join(k.proc, strconv.Itoa(pid), "cgroup")) if err != nil || strings.TrimSpace(string(raw)) != "0::/"+p.Group { return os.ErrPermission } } launcher, err := generationIdentity(main + "/exe") if err != nil || launcher != p.LauncherIdentity { return os.ErrPermission } privatePID := "" for _, ns := range []string{"mnt", "pid"} { want, err := os.Readlink(tenant + "/ns/" + ns) if err != nil || want == "" { return os.ErrPermission } if ns == "pid" { privatePID = want } outer, err := os.Readlink(k.proc + "/self/ns/" + ns) if err != nil || outer == want { return os.ErrPermission } for _, task := range []string{main, path} { actual, err := os.Readlink(task + "/ns/" + ns) if err != nil || actual != want { return os.ErrPermission } } } bound := strings.TrimPrefix(k.boundApp, "/") if bound == "" { bound = "opt/carlos/app" } id, err := generationIdentity(filepath.Join(path, "root", bound)) if err != nil || id != b.Executable { return os.ErrPermission } tree := filepath.Dir(b.Tree) if b.Binding.Account != "" && b.Binding.Account != release.OpsAccount { tree = filepath.Dir(tree) } if filepath.Base(tree) != "versions" || b.Path != release.VersionPath(filepath.Dir(tree), b.Binding.Account, b.Version, b.Binding.App) { return os.ErrPermission } // The canonical version store must be hidden, not merely read-only. if _, err := os.Stat(filepath.Join(path, "root", tree)); !os.IsNotExist(err) { return os.ErrPermission } raw, err := generationRead(path + "/mountinfo") if err != nil { return err } boundMount := false for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { f := strings.Fields(line) if len(f) < 10 { return os.ErrPermission } source, e1 := generationUnescape(f[3]) point, e2 := generationUnescape(f[4]) if e1 != nil || e2 != nil || !filepath.IsAbs(source) || !filepath.IsAbs(point) { return os.ErrPermission } separator := -1 for i := 6; i < len(f); i++ { if f[i] == "-" { separator = i break } } if separator < 6 || len(f) != separator+4 { return os.ErrPermission } if f[separator+1] == "proc" { if source == "/" { visible, err := os.Readlink(filepath.Join(path, "root", point, "1/ns/pid")) if err != nil || visible != privatePID { return os.ErrPermission } } else if source != "/sys" && !strings.HasPrefix(source, "/sys/") { return os.ErrPermission } } major, minor, ok := strings.Cut(f[2], ":") maj, e3 := strconv.ParseUint(major, 10, 32) min, e4 := strconv.ParseUint(minor, 10, 32) if !ok || e3 != nil || e4 != nil { return os.ErrPermission } if unix.Mkdev(uint32(maj), uint32(min)) != b.Executable.Device { continue } // The one reserved file is the only staged bind this namespace may hold. if source == b.Path && point == "/"+bound { boundMount = true continue } below, e1 := filepath.Rel(tree, source) above, e2 := filepath.Rel(source, tree) if (e1 == nil && filepath.IsLocal(below)) || (e2 == nil && filepath.IsLocal(above) && point != "/") { return os.ErrPermission } } if !boundMount { return os.ErrPermission } return nil } func (k generationKernel) replacedGroup(ctx context.Context, old release.GenerationReservation) error { if !old.Closed || !isolatedGeneration(old) || old.Process == (release.GenerationProcess{}) { return os.ErrPermission } boot, err := k.boot() if err != nil { return err } for _, next := range k.contained { if next.Closed || next.Retirement != "" || !isolatedGeneration(next) || next.Process.Containment != 1 || next.Binding.Binding != old.Binding.Binding || next.Binding.Attempt == old.Binding.Attempt || next.Process.Invocation == "" || next.Process.Invocation == old.Process.Invocation || next.Process.Group != old.Process.Group || next.Binding.Target.BootID != boot { continue } if old.Binding.Target.BootID == boot && next.Process.GroupIdentity == old.Process.GroupIdentity { continue } path := filepath.Join(k.proc, strconv.Itoa(next.Process.PID)) status, err := generationRead(path + "/status") if err != nil { return err } k.contained = []release.GenerationReservation{next} ok, err := k.containedTask(ctx, path, status) if err != nil || !ok { return os.ErrPermission } return k.emptyGroups(ctx, filepath.Join(k.cgroups, next.Process.Group)) } return os.ErrPermission }