package main // The after-upgrade check, its second part (novox/hq ADR 0508 ยง4): the processes still mapping a file the upgrade // replaced, named by their unit or container and their executable's path, never their command line or // environment; and whether every DKMS module is built for every installed kernel. Reading another process's // mappings needs root, so the root helper reads them (ADR 0510's `maps`), with MappedDeleted below. import ( "os" "path" "regexp" "sort" "strconv" "strings" ) // ProcFS is the process table as MappedDeleted reads it: the machine's /proc, or a stand-in in a test. type ProcFS interface { ReadDir(dir string) ([]string, error) ReadFile(name string) ([]byte, error) Readlink(name string) (string, error) } // osProc is the machine's own /proc. type osProc struct{} func (osProc) ReadDir(dir string) ([]string, error) { entries, err := os.ReadDir(dir) var names []string for _, e := range entries { names = append(names, e.Name()) } return names, err } func (osProc) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } func (osProc) Readlink(name string) (string, error) { return os.Readlink(name) } // replacedPrefixes are where a package's files are: a mapping deleted elsewhere (a temporary file, shared // memory) is no file an upgrade replaced. var replacedPrefixes = []string{"/usr/", "/opt/"} var containerScope = regexp.MustCompile(`(?:docker|libpod|crio|cri-containerd)-([0-9a-f]{12})[0-9a-f]*\.scope$`) // cgroupOwner names a process's unit or container from its cgroup line `0::/system.slice/x.service`. func cgroupOwner(cgroup string) (unit, container string) { for _, l := range strings.Split(cgroup, "\n") { _, p, ok := strings.Cut(l, "::") if !ok { continue } parts := strings.Split(strings.Trim(p, "/"), "/") for i := len(parts) - 1; i >= 0; i-- { if g := containerScope.FindStringSubmatch(parts[i]); g != nil { return "", g[1] } if i > 0 && parts[i-1] == "docker" && len(parts[i]) >= 12 { return "", parts[i][:12] } } for i := len(parts) - 1; i >= 0; i-- { if strings.HasSuffix(parts[i], ".service") || strings.HasSuffix(parts[i], ".scope") { return parts[i], "" } } } return "", "" } // MappedDeleted reads the process table under procRoot for processes mapping a deleted package file, each named // once by its unit or container and its executable, sorted. func MappedDeleted(fs ProcFS, procRoot string) ([]Mapped, error) { names, err := fs.ReadDir(procRoot) if err != nil { return nil, err } seen := map[Mapped]bool{} out := []Mapped{} for _, name := range names { if _, err := strconv.Atoi(name); err != nil { continue } dir := path.Join(procRoot, name) maps, err := fs.ReadFile(dir + "/maps") if err != nil { continue // the process ended meanwhile } replaced := false for _, l := range strings.Split(string(maps), "\n") { if !strings.HasSuffix(l, " (deleted)") { continue } f := strings.Fields(strings.TrimSuffix(l, " (deleted)")) if len(f) < 6 { continue } for _, p := range replacedPrefixes { if strings.HasPrefix(f[5], p) { replaced = true } } } if !replaced { continue } exe, _ := fs.Readlink(dir + "/exe") cgroup, _ := fs.ReadFile(dir + "/cgroup") unit, container := cgroupOwner(string(cgroup)) m := Mapped{Unit: unit, Container: container, Executable: strings.TrimSuffix(exe, " (deleted)")} if !seen[m] { seen[m] = true out = append(out, m) } } sort.Slice(out, func(i, j int) bool { a, b := out[i], out[j] if a.Unit+a.Container != b.Unit+b.Container { return a.Unit+a.Container < b.Unit+b.Container } return a.Executable < b.Executable }) return out, nil } // unbuiltDKMS is every DKMS module built for some kernel and not for an installed one its own bound allows. func (m *Machine) unbuiltDKMS() ([]DKMSBuild, error) { modules, err := m.DKMSModules() if err != nil { return nil, err } kernels, err := m.installedKernels() if err != nil { return nil, err } out := []DKMSBuild{} for _, mod := range modules { if len(mod.Kernels) == 0 { continue // added and never built: not a module this machine uses } bound := m.dkmsBound(mod) for _, k := range kernels { if contains(mod.Kernels, k) { continue } if held, _ := heldBy(bound, k); held != "" { continue } out = append(out, DKMSBuild{Module: mod.Module, Version: mod.Version, Kernel: k}) } } return out, nil } // afterCheckII fills the check's second part and judges it: a DKMS module not built for a kernel fails it; // a process still mapping a replaced file is said and does not. func (u *Upgrades) afterCheckII(c *Check) { if u.Root == nil { c.MappedError = "no root helper to read the processes' mapped files" } else if mapped, err := u.Root.Maps(); err != nil { c.MappedError = err.Error() } else { c.Mapped = mapped } unbuilt, err := u.Machine.unbuiltDKMS() if err != nil { c.DKMSError = err.Error() c.Passed = false return } c.Unbuilt = unbuilt if len(unbuilt) > 0 { c.Passed = false } }