174 lines
5.0 KiB
Go
174 lines
5.0 KiB
Go
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
|
|
}
|
|
}
|