| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "secanomalyd/processes.h" |
| |
| #include <dirent.h> |
| #include <fcntl.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <cstdio> |
| #include <iterator> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include <absl/cleanup/cleanup.h> |
| #include <base/files/file_path.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/posix/eintr_wrapper.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_tokenizer.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <re2/re2.h> |
| |
| #include <brillo/process/process.h> |
| |
| namespace secanomalyd { |
| |
| namespace { |
| |
| constexpr pid_t kInitPid = 1; |
| constexpr pid_t kKThreadDPid = 2; |
| |
| constexpr char kProcSubdirPattern[] = "[0-9]*"; |
| constexpr char kMinijailExecName[] = "minijail0"; |
| |
| const base::FilePath kProcStatusFile("status"); |
| const base::FilePath kProcCmdlineFile("cmdline"); |
| |
| const base::FilePath kProcNsPidPath("ns/pid"); |
| const base::FilePath kProcNsMntPath("ns/mnt"); |
| const base::FilePath kProcNsUserPath("ns/user"); |
| |
| static constexpr LazyRE2 kProcNsPattern = {R"([a-z]+:\[(\d+)\])"}; |
| |
| constexpr char kSecCompModeDisabled[] = "0"; |
| // SECCOMP_MODE_STRICT is 1. |
| // SECCOMP_MODE_FILTER is 2. |
| |
| constexpr uint64_t kCapSysAdminMask = 1 << 21; |
| |
| // Reads a file under a directory, given the FD for the directory. This is |
| // useful for when the OS reuses a PID, in which case the underlying FD becomes |
| // invalidated and the process is skipped. |
| static bool ReadFileRelativeToDirFD(const int dir_fd, |
| const base::FilePath& filename, |
| std::string* content_ptr) { |
| int fd = HANDLE_EINTR( |
| openat(dir_fd, filename.value().c_str(), O_RDONLY, O_CLOEXEC)); |
| if (fd == -1) { |
| PLOG(ERROR) << "openat(" << filename << ") failed"; |
| return false; |
| } |
| |
| // Convert the fd to FILE immediately to avoid leaking fd. |
| base::ScopedFILE fs = base::ScopedFILE(fdopen(fd, "r")); |
| if (!fs) { |
| PLOG(ERROR) << "Failed to obtain FD for " << filename << " file"; |
| close(fd); |
| return false; |
| } |
| |
| if (!base::ReadStreamToString(fs.get(), content_ptr)) { |
| LOG(ERROR) << "ReadStreamToString failed on " << filename; |
| return false; |
| } |
| return true; |
| } |
| |
| // Kernel arg and env lists use '\0' to delimit elements. |
| static std::string SafeTransFromArgvEnvp(const std::string cmdline) { |
| std::string res; |
| base::StringTokenizer t(cmdline, std::string("\0", 1)); |
| while (t.GetNext()) { |
| res.append(base::StringPrintf("%s ", t.token().c_str())); |
| } |
| if (res.length() > 0) { |
| res.pop_back(); |
| } |
| return res; |
| } |
| |
| static ino_t GetNsFromPath(const base::FilePath& ns_symlink_path) { |
| // *_ns_symlink are not actually pathlike. E.g: "mnt:[4026531840]". |
| base::FilePath ns_symlink; |
| std::string ns_string; |
| ino_t ns; |
| if (!base::ReadSymbolicLink(ns_symlink_path, &ns_symlink) || |
| !RE2::FullMatch(ns_symlink.value(), *kProcNsPattern, &ns_string) || |
| !base::StringToUint64(ns_string, &ns)) { |
| return 0; |
| } |
| return ns; |
| } |
| |
| } // namespace |
| |
| MaybeProcEntry ProcEntry::CreateFromPath(const base::FilePath& pid_path) { |
| // ProcEntry attributes. |
| pid_t pid, ppid; |
| ino_t pidns = 0, mntns = 0, usrns = 0; |
| std::string comm, args; |
| SandboxStatus sandbox_status; |
| sandbox_status.reset(); |
| |
| // Fail if we cannot parse a PID from the supplied path. |
| if (!base::StringToInt(pid_path.BaseName().value(), &pid)) { |
| LOG(ERROR) << "Could not parse a PID from path " << pid_path; |
| return std::nullopt; |
| } |
| |
| DIR* pid_dir_ptr = opendir(pid_path.value().c_str()); |
| if (!pid_dir_ptr) { |
| PLOG(ERROR) << "opendir(" << pid_path << ") failed"; |
| return std::nullopt; |
| } |
| |
| absl::Cleanup close_dir = [=] { |
| if (closedir(pid_dir_ptr) == -1) |
| PLOG(ERROR) << "Failed to close dir " << pid_path; |
| }; |
| |
| int pid_dir_fd = HANDLE_EINTR(dirfd(pid_dir_ptr)); |
| if (pid_dir_fd == -1) { |
| LOG(ERROR) << "Failed to obtain FD for " << pid_path; |
| return std::nullopt; |
| } |
| |
| // Fail if we cannot read the status file, since just a PID is not useful. |
| std::string status_file_content; |
| if (!ReadFileRelativeToDirFD(pid_dir_fd, kProcStatusFile, |
| &status_file_content)) { |
| return std::nullopt; |
| } |
| |
| // The /proc/pid/status file follows this format: |
| // Attribute:\tValue\nAttribute:\tValue\n... |
| // In cases where an attribute has several values, each value is separated |
| // with a tab: Attribute:\tValue1\tValue2\tValue3\n... |
| // See https://man7.org/linux/man-pages/man5/proc.5.html for the list of |
| // attributes in this file. |
| // In our case we parse the values of `Name`, `PPid`, `Uid`, `CapEff`, |
| // `NoNewPrivs` and `Seccomp`. |
| base::StringTokenizer t(status_file_content, "\n"); |
| while (t.GetNext()) { |
| std::string_view line = t.token_piece(); |
| if (base::StartsWith(line, "Name:")) { |
| comm = std::string(line.substr(line.rfind("\t") + 1)); |
| } |
| if (base::StartsWith(line, "PPid:")) { |
| if (!base::StringToInt(std::string(line.substr(line.rfind("\t") + 1)), |
| &ppid)) { |
| ppid = 0; |
| } |
| } |
| if (base::StartsWith(line, "Uid:")) { |
| // The UID field includes real, effective, saved set and filesystem UIDs. |
| // We use the real UID to determine whether the process is running as |
| // root. |
| std::string_view all_uids = line.substr(line.find("\t") + 1); |
| size_t real_uid_len = |
| all_uids.length() - all_uids.substr(all_uids.find("\t")).length(); |
| if (std::string(all_uids.substr(0, real_uid_len)) != "0") { |
| sandbox_status.set(kNonRootBit); |
| } |
| } |
| if (base::StartsWith(line, "CapEff:")) { |
| uint64_t cap_eff_hex; |
| if (base::HexStringToUInt64(line.substr(line.rfind("\t") + 1), |
| &cap_eff_hex) && |
| (cap_eff_hex & kCapSysAdminMask) == 0) { |
| sandbox_status.set(kNoCapSysAdminBit); |
| } |
| } |
| if (base::StartsWith(line, "NoNewPrivs:") && |
| line.substr(line.rfind("\t") + 1) == "1") |
| // For more information on no new privs see |
| // https://www.kernel.org/doc/html/v4.19/userspace-api/no_new_privs.html |
| sandbox_status.set(kNoNewPrivsBit); |
| if (base::StartsWith(line, "Seccomp:") && |
| line.substr(line.rfind("\t") + 1) != kSecCompModeDisabled) |
| sandbox_status.set(kSecCompBit); |
| } |
| |
| // Fail if we cannot read the status file, since just a PID is not useful. |
| std::string cmdline_file_content; |
| if (ReadFileRelativeToDirFD(pid_dir_fd, kProcCmdlineFile, |
| &cmdline_file_content)) { |
| // Reads the rest of the process files before processing any content. |
| |
| if (cmdline_file_content.empty()) { |
| // If there are no args, we set `args` to be be the command name, but |
| // enclosed in square brackets. This is to follow the `ps` convention, and |
| // to avoid having empty lines in the list of processes in crash reports. |
| args = base::StringPrintf("[%s]", comm.c_str()); |
| } else { |
| args = SafeTransFromArgvEnvp(cmdline_file_content); |
| } |
| } |
| |
| pidns = GetNsFromPath(pid_path.Append(kProcNsPidPath)); |
| mntns = GetNsFromPath(pid_path.Append(kProcNsMntPath)); |
| usrns = GetNsFromPath(pid_path.Append(kProcNsUserPath)); |
| |
| return ProcEntry(pid, ppid, pidns, mntns, usrns, comm, args, sandbox_status); |
| } |
| |
| std::string ProcEntry::FullDescription() const { |
| return base::JoinString({comm_, args_}, " "); |
| } |
| |
| MaybeProcEntries ReadProcesses(ProcessFilter filter, |
| const base::FilePath& proc) { |
| ProcEntries all_entries; |
| base::FileEnumerator proc_enumerator(proc, /*Recursive=*/false, |
| base::FileEnumerator::DIRECTORIES, |
| kProcSubdirPattern); |
| for (base::FilePath pid_path = proc_enumerator.Next(); !pid_path.empty(); |
| pid_path = proc_enumerator.Next()) { |
| MaybeProcEntry entry = ProcEntry::CreateFromPath(pid_path); |
| if (entry) { |
| all_entries.push_back(*entry); |
| } |
| } |
| |
| if (all_entries.empty()) { |
| return std::nullopt; |
| } |
| |
| if (filter == ProcessFilter::kAll) { |
| return MaybeProcEntries(all_entries); |
| } |
| |
| ProcEntries filtered_entries; |
| if (filter == ProcessFilter::kNoKernelTasks) { |
| FilterKernelProcesses(all_entries, filtered_entries); |
| } else if (filter == ProcessFilter::kInitPidNamespaceOnly) { |
| return FilterNonInitPidNsProcesses(all_entries, filtered_entries) |
| ? MaybeProcEntries(filtered_entries) |
| : std::nullopt; |
| } |
| |
| return MaybeProcEntries(filtered_entries); |
| } |
| |
| void FilterKernelProcesses(const ProcEntries& all_procs, |
| ProcEntries& filtered_procs) { |
| // Keeps processes that do not have the kernel thread as their parent and are |
| // not the kernel thread itself. |
| std::copy_if(all_procs.begin(), all_procs.end(), |
| std::back_inserter(filtered_procs), [](const ProcEntry& pe) { |
| return pe.ppid() != kKThreadDPid && pe.pid() != kKThreadDPid; |
| }); |
| } |
| |
| bool FilterNonInitPidNsProcesses(const ProcEntries& all_procs, |
| ProcEntries& filtered_procs) { |
| // Looks for the init process. Fails if no init process is found. |
| MaybeProcEntry init_proc = GetInitProcEntry(all_procs); |
| if (!init_proc) { |
| return false; |
| } |
| |
| // Keeps all processes whose |pidns| does not match init's. |
| ino_t init_pidns = init_proc.value().pidns(); |
| std::copy_if( |
| all_procs.begin(), all_procs.end(), std::back_inserter(filtered_procs), |
| [init_pidns](const ProcEntry& pe) { return pe.pidns() == init_pidns; }); |
| return true; |
| } |
| |
| MaybeProcEntry GetInitProcEntry(const ProcEntries& proc_entries) { |
| for (auto const& e : proc_entries) { |
| if (e.pid() == kInitPid) { |
| return MaybeProcEntry(e); |
| } |
| } |
| LOG(ERROR) << "Failed to find init process"; |
| return std::nullopt; |
| } |
| |
| bool IsProcInForbiddenIntersection(const ProcEntry& proc, |
| const ProcEntry& init_proc) { |
| if (proc.comm() == kMinijailExecName) { |
| return false; |
| } |
| // The process is properly sandboxed if at least one of these conditions is |
| // met: |
| // - The process is not running as root and does not have CAP_SYS_ADMIN in |
| // the init user namespace. |
| // - The process is not in the init PID and mount namespace; |
| // - The process is covered by SecComp. |
| if (proc.sandbox_status()[ProcEntry::kNonRootBit] && |
| (proc.sandbox_status()[ProcEntry::kNoCapSysAdminBit] || |
| proc.userns() != init_proc.userns())) { |
| return false; |
| } |
| if (proc.mntns() != init_proc.mntns() && proc.pidns() != init_proc.pidns()) { |
| return false; |
| } |
| if (proc.sandbox_status()[ProcEntry::kSecCompBit]) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace secanomalyd |