//go:build linux package main // The three things the noise filter needs from the platform: how old a process is, // which processes hold a listening socket, and what the package database says // about an executable. Linux can answer all three; delta_other.go answers none // of them, and deltanoise.go treats "cannot answer" as "report it". import ( "crypto/md5" "encoding/hex" "io" "os" "path/filepath" "strconv" "strings" "time" ) // deltaClockTicks is the kernel's USER_HZ, which /proc//stat's start time // is counted in. Go exposes no sysconf(_SC_CLK_TCK), and the value has been 100 // on every mainstream Linux architecture since the 2.6 era — it is part of the // /proc ABI rather than a tunable, because changing it would reinterpret every // field userspace already reads. // // A wrong constant here would only ever mis-time the short-lived rule, which // holds a finding for one pass. It cannot hide one. const deltaClockTicks = 100 // deltaAgeReader reads the boot clock ONCE and returns the age function for // this pass. Per-pid it is then a single small read of /proc//stat. func deltaAgeReader() func(int) (time.Duration, bool) { raw, err := os.ReadFile("/proc/uptime") if err != nil { return func(int) (time.Duration, bool) { return 0, false } } f := strings.Fields(string(raw)) if len(f) == 0 { return func(int) (time.Duration, bool) { return 0, false } } up, err := strconv.ParseFloat(f[0], 64) if err != nil || up <= 0 { return func(int) (time.Duration, bool) { return 0, false } } return func(pid int) (time.Duration, bool) { stat, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") if err != nil { // GONE IS YOUNG, and the two used to be one answer. The filter runs // 50-100 ms after the walk — a parent map, the package database and // two `ss` execs later — so a process that exited in between is // unreadable here for the most ordinary reason there is. Treating // that as "cannot tell" reported the shortest-lived processes on the // box at warn instead of counting them, which is the exact // population the short-lived rule exists to keep out of the list. // // Anything else — a permission error, a /proc that cannot be read — // stays "cannot tell", which the caller treats as report-it. if os.IsNotExist(err) { return 0, true } return 0, false } ticks, ok := parseProcStatStartTicks(string(stat)) if !ok { return 0, false } started := float64(ticks) / deltaClockTicks age := up - started if age < 0 { // The process started after the uptime read — this pass raced it. // Zero, not an error: it IS that young. age = 0 } return time.Duration(age * float64(time.Second)), true } } // deltaListeningPIDs is the set of processes holding a listening socket, read // through the observe plane's own socket reader so this and `observe:sockets` // can never disagree about what is listening. // deltaSockets is readSockets as a var so a test can hand this a partial table // — the state a live `ss` timeout produces and nothing else can reproduce. var deltaSockets = readSockets func deltaListeningPIDs() (map[int]bool, bool) { r := deltaSockets() // PARTIAL IS UNREADABLE, by this file's own rule. readSockets returns // Supported with HALF a table when `ss` runs out of budget // (observe_ops_linux.go:160-168), and a half table would let the short-lived // rule check its listener exemption against a set that may simply not // contain the socket. An exemption that cannot be checked is not an // exemption. // A FAILED RUN JOINS THE PARTIAL ONE, for the same reason: `ss` exiting // non-zero yields an empty table that would silently answer "nothing is // listening". if !r.Supported || r.Partial != "" || r.Failed != "" { return nil, false } out := map[int]bool{} for _, s := range r.Sockets { if s.PID > 0 { out[s.PID] = true } } return out, true } // ── the package database ──────────────────────────────────────────────────── const dpkgInfoDir = "/var/lib/dpkg/info" // deltaPackagedLookup answers, for the executables asked about, which ones dpkg // owns and still matches byte for byte. // // IT IS AN MD5 COMPARISON BECAUSE THAT IS WHAT DPKG RECORDS. `dpkg -V` verifies // against these same .md5sums files; there is no stronger digest in the // database to check against. So a chosen-prefix collision against a // distribution binary would earn a `known` tier here — and that is the whole // prize, because this sets a TIER and never removes a finding: the level table drops a // known-tier proc_new from warn to notice, and a process with a model endpoint // or an agent signature stays high whatever the package database says // (deltaLevel). The bound on being wrong is one level, not silence. // // Returns nil on a box with no dpkg database, which leaves every candidate at // its undemoted level. func deltaPackagedLookup(paths []string) map[string]string { files, err := filepath.Glob(filepath.Join(dpkgInfoDir, "*.md5sums")) if err != nil || len(files) == 0 { return nil } // The wanted paths, in dpkg's spelling (relative to /, no leading slash), // mapped back to what the caller asked about. want := map[string]string{} for _, p := range paths { for _, k := range dpkgPathForms(p) { want[k] = p } } type hit struct{ pkg, sum string } found := map[string]hit{} for _, f := range files { b, err := os.ReadFile(f) if err != nil { continue } pkg := strings.TrimSuffix(filepath.Base(f), ".md5sums") for _, line := range strings.Split(string(b), "\n") { // "<32 hex> " — two spaces, and the path may contain them. if len(line) < 34 { continue } sum, rest := line[:32], line[32:] rest = strings.TrimLeft(rest, " ") orig, ok := want[rest] if !ok { continue } if _, already := found[orig]; !already { found[orig] = hit{pkg: pkg, sum: strings.ToLower(sum)} } } } if len(found) == 0 { return nil } out := map[string]string{} for path, h := range found { if fileMD5(path) == h.sum { out[path] = h.pkg } } return out } // dpkgPathForms is the path as dpkg might have recorded it. THE /usr MERGE IS // WHY THIS IS A LIST: the kernel reports /usr/bin/ls while a package built // before the merge records bin/ls, and matching only one spelling would leave // half a distribution's binaries reading as unowned — a filter that quietly // stops filtering. func dpkgPathForms(p string) []string { p = strings.TrimPrefix(p, "/") if p == "" { return nil } out := []string{p} switch { case strings.HasPrefix(p, "usr/"): out = append(out, strings.TrimPrefix(p, "usr/")) case strings.HasPrefix(p, "bin/"), strings.HasPrefix(p, "sbin/"), strings.HasPrefix(p, "lib/"): out = append(out, "usr/"+p) } return out } func fileMD5(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() h := md5.New() if _, err := io.Copy(h, f); err != nil { return "" } return hex.EncodeToString(h.Sum(nil)) } // deltaPackagedFn is nil where there is no package database to ask, which is // what turns the rule off rather than having it answer "not packaged" about // every file on a box that simply uses a different packager. func deltaPackagedFn() func([]string) map[string]string { if fi, err := os.Stat(dpkgInfoDir); err != nil || !fi.IsDir() { return nil } return deltaPackagedLookup }