| // Copyright 2019 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package security |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| |
| "github.com/shirou/gopsutil/v3/process" |
| |
| "go.chromium.org/tast-tests/cros/local/asan" |
| "go.chromium.org/tast-tests/cros/local/bundles/cros/security/sandboxing" |
| "go.chromium.org/tast-tests/cros/local/moblab" |
| "go.chromium.org/tast-tests/cros/local/sysutil" |
| "go.chromium.org/tast-tests/cros/local/upstart" |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/testing" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: SandboxedServices, |
| Desc: "Verify running processes' sandboxing status against a baseline", |
| Contacts: []string{ |
| "chromeos-hardening@google.com", |
| }, |
| BugComponent: "b:1040049", |
| Attr: []string{"group:cq-medium", "group:mainline"}, |
| }) |
| } |
| |
| func SandboxedServices(ctx context.Context, s *testing.State) { |
| type feature int // security feature that may be set on a process |
| const ( |
| pidNS feature = 1 << iota // process runs in unique PID namespace |
| mntNS // process runs in unique mount namespace with pivot_root(2) |
| mntNSNoPivotRoot // like mntNS, but pivot_root() not required |
| restrictCaps // process runs with restricted capabilities |
| noNewPrivs // process runs with no_new_privs set (see "minijail -N") |
| seccomp // process runs with a seccomp filter |
| ) |
| |
| // procReqs holds sandboxing requirements for a process. |
| type procReqs struct { |
| name string // process name from "Name:" in /proc/<pid>/status (long names will be truncated) |
| euser, egroup string // effective user and group (either username or numeric ID) |
| features feature // bitfield of security features enabled for the process |
| } |
| |
| // baseline maps from process names (from the "Name:" field in /proc/<pid>/status) |
| // to expected sandboxing features. Every root process must be listed here; non-root process will |
| // also be checked if listed. Other non-root processes, and entries listed here that aren't running, |
| // will be ignored. A single process name may be listed multiple times with different users. |
| baseline := []*procReqs{ |
| {"udevd", "root", "root", 0}, // needs root to create device nodes and change owners/perms |
| {"frecon", "root", "frecon", 0}, // needs root and no namespacing to launch shells |
| {"arc_manager", "root", "root", 0}, |
| {"session_manager", "root", "root", 0}, |
| {"rsyslogd", "syslog", "syslog", mntNS | restrictCaps}, |
| {"systemd-journal", "syslog", "syslog", mntNS | restrictCaps}, |
| {"dbus-daemon", "messagebus", "messagebus", restrictCaps}, |
| {"wpa_supplicant", "wpa", "wpa", restrictCaps | noNewPrivs}, |
| {"shill", "shill", "shill", restrictCaps | noNewPrivs}, |
| {"chapsd", "chaps", "chronos-access", restrictCaps | noNewPrivs}, |
| {"cryptohomed", "root", "root", 0}, |
| {"cryptohome-namespace-mounter", "root", "root", 0}, |
| {"powerd", "power", "power", restrictCaps}, |
| {"ModemManager", "modem", "modem", restrictCaps | noNewPrivs}, |
| {"dhcpcd", "dhcp", "dhcp", restrictCaps}, |
| {"metrics_daemon", "metrics", "metrics", 0}, |
| {"disks", "cros-disks", "cros-disks", restrictCaps | noNewPrivs}, |
| {"update_engine", "root", "root", 0}, |
| {"update_engine_client", "root", "root", 0}, |
| {"bluetoothd", "bluetooth", "bluetooth", restrictCaps | noNewPrivs}, |
| {"btadapterd", "bluetooth", "bluetooth", restrictCaps | noNewPrivs}, |
| {"btmanagerd", "bluetooth", "bluetooth", restrictCaps | noNewPrivs}, |
| {"debugd", "root", "root", mntNS}, |
| {"cras", "cras", "cras", mntNS | restrictCaps | noNewPrivs}, |
| {"tcsd", "tss", "tss", restrictCaps}, |
| {"mtpd", "mtp", "mtp", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"tlsdated", "tlsdate", "tlsdate", restrictCaps}, |
| {"tlsdated-setter", "root", "root", noNewPrivs | seccomp}, |
| {"thermal.sh", "root", "root", 0}, |
| {"daisydog", "watchdog", "watchdog", pidNS | mntNS | restrictCaps | noNewPrivs}, |
| {"permission_broker", "devbroker", "root", restrictCaps | noNewPrivs}, |
| {"netfilter-queue", "nfqueue", "nfqueue", restrictCaps | seccomp}, |
| {"anomaly_detector", "root", "syslog", 0}, |
| {"attestationd", "attestation", "attestation", restrictCaps | noNewPrivs | seccomp}, |
| {"pca_agentd", "attestation", "attestation", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"periodic_scheduler", "root", "root", 0}, |
| {"metrics_client", "root", "root", 0}, |
| {"ipf_ufd", "root", "root", 0}, |
| {"ipfhostd", "daemon", "daemon", 0}, |
| {"easy_unlock", "easy-unlock", "easy-unlock", 0}, |
| {"sslh-fork", "sslh", "sslh", pidNS | mntNS | restrictCaps | seccomp}, |
| {"upstart-socket-bridge", "root", "root", 0}, |
| {"timberslide", "root", "root", 0}, |
| {"timberslide-watcher.sh", "root", "root", 0}, |
| {"auditd", "root", "root", 0}, |
| {"firewalld", "firewall", "firewall", pidNS | mntNS | restrictCaps | noNewPrivs}, |
| {"conntrackd", "nfqueue", "nfqueue", mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"avahi-daemon", "avahi", "avahi", restrictCaps}, |
| {"upstart-udev-bridge", "root", "root", 0}, |
| {"midis", "midis", "midis", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"bio_crypto_init", "biod", "biod", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"biod", "biod", "biod", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"cros_camera_service", "arc-camera", "arc-camera", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"cros_camera_algo", "arc-camera", "arc-camera", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"arc_camera_service", "arc-camera", "arc-camera", restrictCaps}, |
| {"arc-obb-mounter", "root", "root", pidNS | mntNS}, |
| {"cdm-oemcrypto", "cdm-oemcrypto", "cdm-oemcrypto", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"cdm-oemcrypto-wv14", "cdm-oemcrypto", "cdm-oemcrypto", pidNS | mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"udevadm", "root", "root", 0}, |
| {"usb_bouncer", "root", "root", 0}, |
| {"brcm_patchram_plus", "root", "root", 0}, // runs on some veyron boards |
| {"os_install_service", "root", "root", 0}, // runs on reven |
| {"rialto_cellular_autoconnect", "root", "root", 0}, // runs on veyron_rialto |
| {"rialto_modem_watchdog", "root", "root", 0}, // runs on veyron_rialto |
| {"netperf", "root", "root", 0}, // started by Autotest tests |
| {"tpm_managerd", "tpm_manager", "tpm_manager", restrictCaps | noNewPrivs | seccomp}, |
| {"trunksd", "trunks", "trunks", restrictCaps | noNewPrivs | seccomp}, |
| {"imageloader", "root", "root", 0}, // uses NNP/seccomp but sometimes seen before sandboxing: https://crbug.com/936703#c16 |
| {"imageloader", "imageloaderd", "imageloaderd", mntNSNoPivotRoot | restrictCaps | noNewPrivs | seccomp}, |
| {"patchpaneld", "patchpaneld", "patchpaneld", restrictCaps | noNewPrivs}, |
| {"cros_healthd", "root", "root", mntNS}, // cros_healthd's root-level executor |
| {"cros_healthd", "cros_healthd", "cros_healthd", mntNS | restrictCaps | noNewPrivs | seccomp}, // main cros_healthd daemon |
| {"featured", "root", "root", 0}, |
| {"cr50_disable_sleep", "root", "root", 0}, |
| {"rmad", "root", "root", mntNS}, // rmad's root-level executor |
| {"rmad", "rmad", "rmad", mntNS | restrictCaps | noNewPrivs | seccomp}, // main RMA daemon |
| {"lvmd", "root", "root", 0}, // TODO(b/278480982): reduce privileges allowed for lvmd. |
| {"hiberman", "root", "root", 0}, // TODO(b/293361061): Sandbox hiberman. |
| {"swap_management", "root", "root", 0}, |
| {"odmld", "odml", "odml", mntNS | restrictCaps | noNewPrivs | seccomp}, |
| {"dnsproxyd", "dns-proxy-system", "dns-proxy", restrictCaps | noNewPrivs | seccomp}, |
| {"dnsproxyd", "dns-proxy-user", "dns-proxy", restrictCaps | noNewPrivs | seccomp}, |
| |
| // Processes running with CAP_SYS_ADMIN. |
| {"spaced", "spaced", "spaced", restrictCaps}, |
| {"cros-disks", "cros-disks", "cros-disks", restrictCaps}, |
| {"dnsproxyd", "dns-proxy", "dns-proxy", restrictCaps | noNewPrivs | seccomp}, |
| {"shadercached", "shadercached", "shadercached", restrictCaps}, |
| |
| // These processes run as root in the ARC container. |
| {"app_process", "android-root", "android-root", pidNS | mntNS}, |
| {"debuggerd", "android-root", "android-root", pidNS | mntNS}, |
| {"debuggerd:sig", "android-root", "android-root", pidNS | mntNS}, |
| {"healthd", "android-root", "android-root", pidNS | mntNS}, |
| {"vold", "android-root", "android-root", pidNS | mntNS}, |
| |
| // These processes run as non-root in the ARC container. |
| {"boot_latch", "656360", "656360", pidNS | mntNS | restrictCaps}, |
| {"bugreportd", "657360", "656367", pidNS | mntNS | restrictCaps}, |
| {"logd", "656396", "656396", pidNS | mntNS | restrictCaps}, |
| {"servicemanager", "656360", "656360", pidNS | mntNS | restrictCaps}, |
| {"surfaceflinger", "656360", "656363", pidNS | mntNS | restrictCaps}, |
| |
| // Small, one-off init/setup scripts that don't spawn daemons and that are short-lived. |
| {"activate_date.service", "root", "root", 0}, |
| {"chromeos-trim", "root", "root", 0}, |
| {"crx-import.sh", "root", "root", 0}, |
| {"frecon-pre-start.sh", "root", "root", 0}, |
| {"powerd-pre-start.sh", "root", "root", 0}, |
| {"update_rw_vpd", "root", "root", 0}, |
| {"vpd_get_value", "root", "root", 0}, |
| {"vpd_icc", "root", "root", 0}, |
| {"send-uptime-metrics", "root", "root", 0}, |
| // b/228320883, we don't really care about these Cr50 update/log scripts. |
| // src/platform/cr50/extra/usb_updater/gsctool.c |
| {"gsctool", "root", "root", 0}, |
| // src/platform2/hwsec-utils/src/bin/gsc_flash_log.rs |
| {"gsc_flash_log", "root", "root", 0}, |
| // The attack surface for 'gdbus' is minimal, and these processes are short-lived by nature |
| // since they are waiting for a D-Bus endpoint to come up. |
| {"gdbus", "root", "root", 0}, |
| // 'quipper' is short-lived and used for profiling. |
| {"quipper", "root", "root", 0}, |
| // 'pvs' is a symlink to '/sbin/lvm'. |
| {"pvs", "root", "root", 0}, |
| // 'arc-file-syncer' performs bi-directional file synchronization for the set of |
| // predefined control files. |
| // TODO(b/319667794): Sandbox 'arc-file-syncer' better. |
| {"arc-file-syncer", "root", "root", 0}, |
| |
| // One-off processes that we see when this test runs together with other tests. |
| // src/overlays/overlay-kip/chromeos-base/modem-watchdog/files/chromeos-kip-modem-watchdog.sh |
| {"chromeos-kip-modem-watchdog.sh", "root", "root", 0}, |
| // src/platform2/installer/chromeos-setgoodkernel |
| {"chromeos-setgoodkernel", "root", "root", 0}, |
| {"dbus-send", "root", "root", 0}, |
| // src/third_party/flashrom/ |
| {"flashrom", "root", "root", 0}, |
| // src/platform/factory/sh/goofy_control.sh |
| {"goofy_control.sh", "root", "root", 0}, |
| // src/third_party/chromiumos-overlay/sys-apps/ureadahead/files/init/ureadahead.conf |
| {"ureadahead", "root", "root", 0}, |
| {"sed", "root", "root", 0}, |
| {"start", "root", "root", 0}, |
| {"evtest", "root", "root", 0}, |
| } |
| |
| // Names of processes whose children should be ignored. These processes themselves are also ignored. |
| ignoredAncestorNames := make(map[string]struct{}) |
| for _, ancestorName := range sandboxing.IgnoredAncestors { |
| ignoredAncestorNames[sandboxing.TruncateProcName(ancestorName)] = struct{}{} |
| } |
| |
| if moblab.IsMoblab() { |
| for _, moblabAncestorName := range sandboxing.IgnoredMoblabAncestors { |
| ignoredAncestorNames[sandboxing.TruncateProcName(moblabAncestorName)] = struct{}{} |
| } |
| } |
| |
| baselineMap := make(map[string][]*procReqs, len(baseline)) |
| for _, reqs := range baseline { |
| name := sandboxing.TruncateProcName(reqs.name) |
| baselineMap[name] = append(baselineMap[name], reqs) |
| } |
| for name, rs := range baselineMap { |
| users := make(map[string]struct{}, len(rs)) |
| for _, r := range rs { |
| if _, ok := users[r.euser]; ok { |
| s.Fatalf("Duplicate %q requirements for user %q in baseline", name, r.euser) |
| } |
| users[r.euser] = struct{}{} |
| } |
| } |
| |
| exclusionsMap := make(map[string]struct{}) |
| for _, name := range sandboxing.Exclusions { |
| exclusionsMap[sandboxing.TruncateProcName(name)] = struct{}{} |
| } |
| |
| // parseID first tries to parse str (a procReqs euser or egroup field) as a number. |
| // Failing that, it passes it to lookup, which should be sysutil.GetUID or sysutil.GetGID. |
| parseID := func(str string, lookup func(string) (uint32, error)) (uint32, error) { |
| if id, err := strconv.Atoi(str); err == nil { |
| return uint32(id), nil |
| } |
| if id, err := lookup(str); err == nil { |
| return id, nil |
| } |
| return 0, errors.New("couldn't parse as number and lookup failed") |
| } |
| |
| if upstart.JobExists(ctx, "ui") { |
| s.Log("Restarting ui job to clean up stray processes") |
| if err := upstart.RestartJob(ctx, "ui"); err != nil { |
| s.Fatal("Failed to restart ui job: ", err) |
| } |
| } |
| |
| asanEnabled, err := asan.Enabled(ctx) |
| if err != nil { |
| s.Error("Failed to check if ASan is enabled: ", err) |
| } else if asanEnabled { |
| s.Log("ASan is enabled; will skip seccomp checks") |
| } |
| |
| procs, err := process.Processes() |
| if err != nil { |
| s.Fatal("Failed to list running processes: ", err) |
| } |
| const logName = "processes.txt" |
| s.Logf("Writing %v processes to %v", len(procs), logName) |
| lg, err := os.Create(filepath.Join(s.OutDir(), logName)) |
| if err != nil { |
| s.Fatal("Failed to open log: ", err) |
| } |
| defer lg.Close() |
| |
| // We don't know that we'll see parent processes before their children (since PIDs can wrap around), |
| // so do an initial pass to gather information. |
| infos := make(map[int32]*sandboxing.ProcSandboxInfo) |
| ignoredAncestorPIDs := make(map[int32]struct{}) |
| for _, proc := range procs { |
| info, err := sandboxing.GetProcSandboxInfo(proc) |
| // Even on error, write the partially-filled info to help in debugging. |
| fmt.Fprintf(lg, "%5d %-15s uid=%-6d gid=%-6d pidns=%-10d mntns=%-10d nnp=%-5v seccomp=%-5v ecaps=%#x\n", |
| proc.Pid, info.Name, info.Euid, info.Egid, info.PidNS, info.MntNS, info.NoNewPrivs, info.Seccomp, info.Ecaps) |
| if err != nil { |
| // An error could either indicate that the process exited or that we failed to parse /proc. |
| // Check if the process is still there so we can report the error in the latter case. |
| if status, serr := proc.Status(); serr == nil { |
| s.Errorf("Failed to get info about process %d with status %q: %v", proc.Pid, status, err) |
| } |
| continue |
| } |
| |
| infos[proc.Pid] = info |
| |
| // Determine if all of this process's children should also be ignored. |
| _, ignoredByName := ignoredAncestorNames[info.Name] |
| if ignoredByName || |
| // Assume that any executables under /usr/local are dev- or test-specific, |
| // since /usr/local is mounted noexec if dev mode is disabled. |
| strings.HasPrefix(info.Exe, "/usr/local/") || |
| // Autotest tests sometimes leave orphaned processes running after they exit, |
| // so ignore anything that might e.g. be using a data file from /usr/local/autotest. |
| strings.Contains(info.Cmdline, "autotest") { |
| ignoredAncestorPIDs[proc.Pid] = struct{}{} |
| } |
| } |
| |
| // We use the init process's info later to determine if other |
| // processes have their own capabilities/namespaces or not. |
| const initPID = 1 |
| initInfo := infos[initPID] |
| if initInfo == nil { |
| s.Fatal("Didn't find init process") |
| } |
| |
| s.Logf("Comparing %d processes against baseline", len(infos)) |
| numChecked := 0 |
| for pid, info := range infos { |
| if pid == initPID { |
| continue |
| } |
| if _, ok := exclusionsMap[info.Name]; ok { |
| continue |
| } |
| if _, ok := ignoredAncestorPIDs[pid]; ok { |
| continue |
| } |
| if skip, err := sandboxing.ProcHasAncestor(pid, ignoredAncestorPIDs, infos); err == nil && skip { |
| continue |
| } |
| |
| numChecked++ |
| |
| // We may have expectations for multiple users in the case of a process that forks and drops privileges. |
| var reqs *procReqs |
| var reqUID uint32 |
| for _, r := range baselineMap[info.Name] { |
| uid, err := parseID(r.euser, sysutil.GetUID) |
| if err != nil { |
| s.Errorf("Failed to look up user %q for PID %v", r.euser, pid) |
| continue |
| } |
| // Favor reqs that exactly match the process's EUID, but fall back to the first one we see. |
| match := uid == info.Euid |
| if match || reqs == nil { |
| reqs = r |
| reqUID = uid |
| if match { |
| break |
| } |
| } |
| } |
| |
| if reqs == nil { |
| if info.Euid == 0 { |
| // Processes running as root must always be listed in the baseline. |
| s.Errorf("Unexpected %q process %v (%v) running as root", info.Name, pid, info.Exe) |
| // These failures often correspond to short-lived root processes that are only present |
| // on specific boards. The failures can be hard to fix because we don't always have |
| // access to the failing boards. |
| // However, the failures are normally easy to diagnose by looking up the name of the |
| // process, and checking that it is indeed a short-lived script or tool. |
| // By printing the expected baseline/exclusion entry we make it easier for ourselves |
| // to fix some failures, especially considering that process names are truncated |
| // by the kernel. |
| // {"tpm_managerd", "root", "root", 0}, |
| s.Errorf("A baseline entry for this process would look like: {%q, \"root\", \"root\", 0}", info.Name) |
| s.Errorf("An exclusion list entry for this process would look like: %q", info.Name) |
| } else if info.Ecaps&(1<<sandboxing.CapSysAdmin) > 0 { |
| // Processes running with CAP_SYS_ADMIN must always be listed in the baseline. |
| s.Errorf("Unexpected %q process %v (%v) with CAP_SYS_ADMIN capability", info.Name, pid, info.Exe) |
| // {"spaced", "spaced", "spaced", restrictCaps}, |
| s.Errorf("A baseline entry for this process would look like: {%q, %q, %q, restrictCaps}", info.Name, info.Username, info.Username) |
| s.Errorf("An exclusion list entry for this process would look like: %q", info.Name) |
| } |
| // Ignore unlisted non-root, non-CAP_SYS_ADMIN processes on the |
| // assumption that they've already done some sandboxing. |
| continue |
| } |
| |
| var problems []string |
| |
| if info.Euid != reqUID && reqUID != 0 { |
| problems = append(problems, fmt.Sprintf("effective UID %v; want %v", info.Euid, reqUID)) |
| } |
| |
| if gid, err := parseID(reqs.egroup, sysutil.GetGID); err != nil { |
| s.Errorf("Failed to look up group %q for PID %v", reqs.egroup, pid) |
| } else if info.Egid != gid && gid != 0 { |
| problems = append(problems, fmt.Sprintf("effective GID %v; want %v", info.Egid, gid)) |
| } |
| |
| // We test for PID/mount namespaces and capabilities by comparing against what init is using |
| // since processes inherit these by default. |
| if reqs.features&pidNS != 0 && info.PidNS != -1 && info.PidNS == initInfo.PidNS { |
| problems = append(problems, "missing PID namespace") |
| } |
| if reqs.features&(mntNS|mntNSNoPivotRoot) != 0 && info.MntNS != -1 && info.MntNS == initInfo.MntNS { |
| problems = append(problems, "missing mount namespace") |
| } |
| if reqs.features&restrictCaps != 0 && info.Ecaps == initInfo.Ecaps { |
| problems = append(problems, "no restricted capabilities") |
| } |
| if reqs.features&noNewPrivs != 0 && !info.NoNewPrivs { |
| problems = append(problems, "missing no_new_privs") |
| } |
| // Minijail disables seccomp at runtime when ASan is enabled, so don't check it in that case. |
| if reqs.features&seccomp != 0 && !info.Seccomp && !asanEnabled { |
| problems = append(problems, "seccomp filter disabled") |
| } |
| |
| // If a mount namespace is required and used, but some of the init process's test image mounts |
| // are still present, then the process didn't call pivot_root(). |
| if reqs.features&mntNS != 0 && info.MntNS != -1 && info.MntNS != initInfo.MntNS && info.HasTestImageMounts { |
| problems = append(problems, "did not call pivot_root(2)") |
| } |
| |
| if len(problems) > 0 { |
| s.Errorf("%q process %v (%v) isn't properly sandboxed: %s", |
| info.Name, pid, info.Exe, strings.Join(problems, ", ")) |
| } |
| } |
| |
| s.Logf("Checked %d processes after exclusions", numChecked) |
| } |