From e8db89c57c41dde73fa447bca206faf9075eb484 Mon Sep 17 00:00:00 2001 From: Nick Gregory Date: Mon, 5 Feb 2024 14:30:54 -0500 Subject: [PATCH] ProcessTree: add core process tree logic (1/4) (#1236) * ProcessTree: add core process tree logic * make Step implicitly called by Handle* methods * lint * naming convention * widen pidversion to be generic * move os specific backfill to os specific impl * simplify ts checking * retain/release a whole vec of pids * document processtoken * lint * namespace * add process tree to project-wide unit test target * case change annotations * case change annotations * remove stray comment * default initialize seen_timestamps * fix missing initialization of refcnt and tombstoned * reshuffle pb namespace * pr review * move annotation registration to tree construction * use factory function for tree construction --- Source/santad/BUILD | 1 + Source/santad/ProcessTree/BUILD | 67 ++++ Source/santad/ProcessTree/annotations/BUILD | 11 + .../ProcessTree/annotations/annotator.h | 39 +++ Source/santad/ProcessTree/process.h | 113 +++++++ Source/santad/ProcessTree/process_tree.cc | 300 ++++++++++++++++++ Source/santad/ProcessTree/process_tree.h | 189 +++++++++++ Source/santad/ProcessTree/process_tree.proto | 6 + .../santad/ProcessTree/process_tree_macos.mm | 78 +++++ .../santad/ProcessTree/process_tree_test.mm | 220 +++++++++++++ .../ProcessTree/process_tree_test_helpers.h | 30 ++ .../ProcessTree/process_tree_test_helpers.mm | 42 +++ 12 files changed, 1096 insertions(+) create mode 100644 Source/santad/ProcessTree/BUILD create mode 100644 Source/santad/ProcessTree/annotations/BUILD create mode 100644 Source/santad/ProcessTree/annotations/annotator.h create mode 100644 Source/santad/ProcessTree/process.h create mode 100644 Source/santad/ProcessTree/process_tree.cc create mode 100644 Source/santad/ProcessTree/process_tree.h create mode 100644 Source/santad/ProcessTree/process_tree.proto create mode 100644 Source/santad/ProcessTree/process_tree_macos.mm create mode 100644 Source/santad/ProcessTree/process_tree_test.mm create mode 100644 Source/santad/ProcessTree/process_tree_test_helpers.h create mode 100644 Source/santad/ProcessTree/process_tree_test_helpers.mm diff --git a/Source/santad/BUILD b/Source/santad/BUILD index db9402162..95fdca551 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -1387,6 +1387,7 @@ test_suite( ":SantadTest", ":WatchItemsTest", "//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test", + "//Source/santad/ProcessTree:process_tree_test", ], visibility = ["//:santa_package_group"], ) diff --git a/Source/santad/ProcessTree/BUILD b/Source/santad/ProcessTree/BUILD new file mode 100644 index 000000000..5a178ba73 --- /dev/null +++ b/Source/santad/ProcessTree/BUILD @@ -0,0 +1,67 @@ +load("@rules_cc//cc:defs.bzl", "cc_proto_library") +load("//:helper.bzl", "santa_unit_test") + +package( + default_visibility = ["//:santa_package_group"], +) + +cc_library( + name = "process", + hdrs = ["process.h"], + deps = [ + "//Source/santad/ProcessTree/annotations:annotator", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/synchronization", + ], +) + +objc_library( + name = "process_tree", + srcs = [ + "process_tree.cc", + "process_tree_macos.mm", + ], + hdrs = ["process_tree.h"], + sdk_dylibs = [ + "bsm", + ], + deps = [ + ":process", + "//Source/santad/ProcessTree/annotations:annotator", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/synchronization", + ], +) + +proto_library( + name = "process_tree_proto", + srcs = ["process_tree.proto"], +) + +cc_proto_library( + name = "process_tree_cc_proto", + deps = [":process_tree_proto"], +) + +objc_library( + name = "process_tree_test_helpers", + srcs = ["process_tree_test_helpers.mm"], + hdrs = ["process_tree_test_helpers.h"], + deps = [ + ":process_tree", + "@com_google_absl//absl/synchronization", + ], +) + +santa_unit_test( + name = "process_tree_test", + srcs = ["process_tree_test.mm"], + deps = [ + ":process", + ":process_tree_test_helpers", + "//Source/santad/ProcessTree/annotations:annotator", + ], +) diff --git a/Source/santad/ProcessTree/annotations/BUILD b/Source/santad/ProcessTree/annotations/BUILD new file mode 100644 index 000000000..d7e95b219 --- /dev/null +++ b/Source/santad/ProcessTree/annotations/BUILD @@ -0,0 +1,11 @@ +package( + default_visibility = ["//:santa_package_group"], +) + +cc_library( + name = "annotator", + hdrs = ["Annotator.h"], + deps = [ + "//Source/santad/ProcessTree:process_tree_cc_proto", + ], +) diff --git a/Source/santad/ProcessTree/annotations/annotator.h b/Source/santad/ProcessTree/annotations/annotator.h new file mode 100644 index 000000000..c32b91e26 --- /dev/null +++ b/Source/santad/ProcessTree/annotations/annotator.h @@ -0,0 +1,39 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#ifndef SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H +#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H + +#include + +#include "Source/santad/ProcessTree/process_tree.pb.h" + +namespace santa::santad::process_tree { + +class ProcessTree; +class Process; + +class Annotator { + public: + virtual ~Annotator() = default; + + virtual void AnnotateFork(ProcessTree &tree, const Process &parent, + const Process &child) = 0; + virtual void AnnotateExec(ProcessTree &tree, const Process &orig_process, + const Process &new_process) = 0; + virtual std::optional<::santa::pb::v1::process_tree::Annotations> Proto() const = 0; +}; + +} // namespace santa::santad::process_tree + +#endif diff --git a/Source/santad/ProcessTree/process.h b/Source/santad/ProcessTree/process.h new file mode 100644 index 000000000..ed7df393a --- /dev/null +++ b/Source/santad/ProcessTree/process.h @@ -0,0 +1,113 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#ifndef SANTA__SANTAD_PROCESSTREE_PROCESS_H +#define SANTA__SANTAD_PROCESSTREE_PROCESS_H + +#include + +#include +#include +#include +#include + +#include "Source/santad/ProcessTree/annotations/annotator.h" +#include "absl/container/flat_hash_map.h" + +namespace santa::santad::process_tree { + +struct Pid { + pid_t pid; + uint64_t pidversion; + + friend bool operator==(const struct Pid &lhs, const struct Pid &rhs) { + return lhs.pid == rhs.pid && lhs.pidversion == rhs.pidversion; + } + friend bool operator!=(const struct Pid &lhs, const struct Pid &rhs) { + return !(lhs == rhs); + } +}; + +template +H AbslHashValue(H h, const struct Pid &p) { + return H::combine(std::move(h), p.pid, p.pidversion); +} + +struct Cred { + uid_t uid; + gid_t gid; + + friend bool operator==(const struct Cred &lhs, const struct Cred &rhs) { + return lhs.uid == rhs.uid && lhs.gid == rhs.gid; + } + friend bool operator!=(const struct Cred &lhs, const struct Cred &rhs) { + return !(lhs == rhs); + } +}; + +struct Program { + std::string executable; + std::vector arguments; + + friend bool operator==(const struct Program &lhs, const struct Program &rhs) { + return lhs.executable == rhs.executable && lhs.arguments == rhs.arguments; + } + friend bool operator!=(const struct Program &lhs, const struct Program &rhs) { + return !(lhs == rhs); + } +}; + +// Fwd decls +class ProcessTree; + +class Process { + public: + explicit Process(const Pid pid, const Cred cred, + std::shared_ptr program, + std::shared_ptr parent) + : pid_(pid), + effective_cred_(cred), + program_(program), + annotations_(), + parent_(parent), + refcnt_(0), + tombstoned_(false) {} + Process(const Process &) = default; + Process& operator=(const Process &) = delete; + Process(Process &&) = default; + Process& operator=(Process &&) = delete; + + // Const "attributes" are public + const struct Pid pid_; + const struct Cred effective_cred_; + const std::shared_ptr program_; + + private: + // This is not API. + // The tree helper methods are the API, and we just happen to implement + // annotation storage and the parent relation in memory on the process right + // now. + friend class ProcessTree; + absl::flat_hash_map> + annotations_; + std::shared_ptr parent_; + // TODO(nickmg): atomic here breaks the build. + int refcnt_; + // If the process is tombstoned, the event removing it from the tree has been + // processed, but refcnt>0 keeps it alive. + bool tombstoned_; +}; + +} // namespace santa::santad::process_tree + +#endif diff --git a/Source/santad/ProcessTree/process_tree.cc b/Source/santad/ProcessTree/process_tree.cc new file mode 100644 index 000000000..686e05314 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree.cc @@ -0,0 +1,300 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#include "Source/santad/ProcessTree/process_tree.h" + +#include +#include +#include +#include +#include +#include + +#include "Source/santad/ProcessTree/annotations/annotator.h" +#include "Source/santad/ProcessTree/process.h" +#include "Source/santad/ProcessTree/process_tree.pb.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/synchronization/mutex.h" + +namespace santa::santad::process_tree { + +void ProcessTree::BackfillInsertChildren( + absl::flat_hash_map> &parent_map, + std::shared_ptr parent, const Process &unlinked_proc) { + auto proc = std::make_shared( + unlinked_proc.pid_, unlinked_proc.effective_cred_, + // Re-use shared pointers from parent if value equivalent + (parent && *(unlinked_proc.program_) == *(parent->program_)) + ? parent->program_ + : unlinked_proc.program_, + parent); + { + absl::MutexLock lock(&mtx_); + map_.emplace(unlinked_proc.pid_, proc); + } + + // The only case where we should not have a parent is the root processes + // (e.g. init, kthreadd). + if (parent) { + for (auto &annotator : annotators_) { + annotator->AnnotateFork(*this, *(proc->parent_), *proc); + if (proc->program_ != proc->parent_->program_) { + annotator->AnnotateExec(*this, *(proc->parent_), *proc); + } + } + } + + for (const Process &child : parent_map[unlinked_proc.pid_.pid]) { + BackfillInsertChildren(parent_map, proc, child); + } +} + +void ProcessTree::HandleFork(uint64_t timestamp, const Process &parent, + const Pid new_pid) { + if (Step(timestamp)) { + std::shared_ptr child; + { + absl::MutexLock lock(&mtx_); + child = std::make_shared(new_pid, parent.effective_cred_, + parent.program_, map_[parent.pid_]); + map_.emplace(new_pid, child); + } + for (const auto &annotator : annotators_) { + annotator->AnnotateFork(*this, parent, *child); + } + } +} + +void ProcessTree::HandleExec(uint64_t timestamp, const Process &p, + const Pid new_pid, const Program prog, + const Cred c) { + if (Step(timestamp)) { + // TODO(nickmg): should struct pid be reworked and only pid_version be + // passed? + assert(new_pid.pid == p.pid_.pid); + + auto new_proc = std::make_shared( + new_pid, c, std::make_shared(prog), p.parent_); + { + absl::MutexLock lock(&mtx_); + remove_at_.push_back({timestamp, p.pid_}); + map_.emplace(new_proc->pid_, new_proc); + } + for (const auto &annotator : annotators_) { + annotator->AnnotateExec(*this, p, *new_proc); + } + } +} + +void ProcessTree::HandleExit(uint64_t timestamp, const Process &p) { + if (Step(timestamp)) { + absl::MutexLock lock(&mtx_); + remove_at_.push_back({timestamp, p.pid_}); + } +} + +bool ProcessTree::Step(uint64_t timestamp) { + absl::MutexLock lock(&mtx_); + uint64_t new_cutoff = seen_timestamps_.front(); + if (timestamp < new_cutoff) { + // Event timestamp is before the rolling list of seen events. + // This event may or may not have been processed, but be conservative and + // do not reprocess. + return false; + } + + // seen_timestamps_ is sorted, so only look for the value if it's possibly within the array. + if (timestamp < seen_timestamps_.back()) { + // TODO(nickmg): If array is made bigger, replace with a binary search. + for (const auto seen_ts : seen_timestamps_) { + if (seen_ts == timestamp) { + // Event seen, signal it should not be reprocessed. + return false; + } + } + } + + auto insert_point = + std::find_if(seen_timestamps_.rbegin(), seen_timestamps_.rend(), + [&](uint64_t x) { return x < timestamp; }); + std::move(seen_timestamps_.begin() + 1, insert_point.base(), + seen_timestamps_.begin()); + *insert_point = timestamp; + + for (auto it = remove_at_.begin(); it != remove_at_.end();) { + if (it->first < new_cutoff) { + if (auto target = GetLocked(it->second); + target && (*target)->refcnt_ > 0) { + (*target)->tombstoned_ = true; + } else { + map_.erase(it->second); + } + it = remove_at_.erase(it); + } else { + it++; + } + } + + return true; +} + +void ProcessTree::RetainProcess(std::vector &pids) { + absl::MutexLock lock(&mtx_); + for (const struct Pid &p : pids) { + auto proc = GetLocked(p); + if (proc) { + (*proc)->refcnt_++; + } + } +} + +void ProcessTree::ReleaseProcess(std::vector &pids) { + absl::MutexLock lock(&mtx_); + for (const struct Pid &p : pids) { + auto proc = GetLocked(p); + if (proc) { + if (--(*proc)->refcnt_ == 0 && (*proc)->tombstoned_) { + map_.erase(p); + } + } + } +} + +/* +--- +Annotation get/set +--- +*/ + +void ProcessTree::AnnotateProcess(const Process &p, + std::shared_ptr a) { + absl::MutexLock lock(&mtx_); + const Annotator &x = *a; + map_[p.pid_]->annotations_.emplace(std::type_index(typeid(x)), std::move(a)); +} + +std::optional<::santa::pb::v1::process_tree::Annotations> ProcessTree::ExportAnnotations(const Pid p) { + auto proc = Get(p); + if (!proc || (*proc)->annotations_.size() == 0) { + return std::nullopt; + } + ::santa::pb::v1::process_tree::Annotations a; + for (const auto &[_, annotation] : (*proc)->annotations_) { + if (auto x = annotation->Proto(); x) a.MergeFrom(*x); + } + return a; +} + +/* +--- +Tree inspection methods +--- +*/ + +std::vector> ProcessTree::RootSlice( + std::shared_ptr p) const { + std::vector> slice; + while (p) { + slice.push_back(p); + p = p->parent_; + } + return slice; +} + +void ProcessTree::Iterate( + std::function p)> f) const { + std::vector> procs; + { + absl::ReaderMutexLock lock(&mtx_); + procs.reserve(map_.size()); + for (auto &[_, proc] : map_) { + procs.push_back(proc); + } + } + + for (auto &p : procs) { + f(p); + } +} + +std::optional> ProcessTree::Get( + const Pid target) const { + absl::ReaderMutexLock lock(&mtx_); + return GetLocked(target); +} + +std::optional> ProcessTree::GetLocked( + const Pid target) const { + auto it = map_.find(target); + if (it == map_.end()) { + return std::nullopt; + } + return it->second; +} + +std::shared_ptr ProcessTree::GetParent(const Process &p) const { + return p.parent_; +} + +#if SANTA_PROCESS_TREE_DEBUG +void ProcessTree::DebugDump(std::ostream &stream) const { + absl::ReaderMutexLock lock(&mtx_); + stream << map_.size() << " processes" << std::endl; + DebugDumpLocked(stream, 0, 0); +} + +void ProcessTree::DebugDumpLocked(std::ostream &stream, int depth, + pid_t ppid) const + ABSL_SHARED_LOCKS_REQUIRED(mtx_) { + for (auto &[_, process] : map_) { + if ((ppid == 0 && !process->parent_) || + (process->parent_ && process->parent_->pid_.pid == ppid)) { + stream << std::string(2 * depth, ' ') << process->pid_.pid + << process->program_->executable << std::endl; + DebugDumpLocked(stream, depth + 1, process->pid_.pid); + } + } +} +#endif + +absl::StatusOr> CreateTree(std::vector> &&annotations) { + absl::flat_hash_set seen; + for (const auto &annotator : annotations) { + if (seen.count(std::type_index(typeid(annotator)))) { + return absl::InvalidArgumentError("Multiple annotators of the same class"); + } + seen.emplace(std::type_index(typeid(annotator))); + } + auto tree = std::make_shared(std::move(annotations)); + if (auto status = tree->Backfill(); !status.ok()) { + return status; + } + return tree; +} + +/* +---- +Tokens +---- +*/ + +ProcessToken::ProcessToken(std::shared_ptr tree, + std::vector pids) + : tree_(std::move(tree)), pids_(std::move(pids)) { + tree_->RetainProcess(pids); +} + +ProcessToken::~ProcessToken() { tree_->ReleaseProcess(pids_); } + +} // namespace santa::santad::process_tree diff --git a/Source/santad/ProcessTree/process_tree.h b/Source/santad/ProcessTree/process_tree.h new file mode 100644 index 000000000..0ea262494 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree.h @@ -0,0 +1,189 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#ifndef SANTA__SANTAD_PROCESSTREE_TREE_H +#define SANTA__SANTAD_PROCESSTREE_TREE_H + +#include +#include +#include + +#include "Source/santad/ProcessTree/process.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/synchronization/mutex.h" +#include "process.h" + +namespace santa::santad::process_tree { + +absl::StatusOr LoadPID(pid_t pid); + +// Fwd decl for test peer. +class ProcessTreeTestPeer; + +class ProcessTree { + public: + explicit ProcessTree(std::vector> &&annotators) : annotators_(std::move(annotators)), seen_timestamps_({}) {} + ProcessTree(const ProcessTree &) = delete; + ProcessTree& operator=(const ProcessTree &) = delete; + ProcessTree(ProcessTree &&) = delete; + ProcessTree& operator=(ProcessTree &&) = delete; + + // Initialize the tree with the processes currently running on the system. + absl::Status Backfill(); + + // Inform the tree of a fork event, in which the parent process spawns a child + // with the only difference between the two being the pid. + void HandleFork(uint64_t timestamp, const Process &parent, + const struct Pid child); + + // Inform the tree of an exec event, in which the program and potentially cred + // of a Process change. + // p is the process performing the exec (running the "old" program), + // and new_pid, prog, and cred are the new pid, program, and credentials + // after the exec. + // N.B. new_pid is required as the "pid version" will have changed. + // It is a programming error to pass a new_pid such that + // p.pid_.pid != new_pid.pid. + void HandleExec(uint64_t timestamp, const Process &p, + const struct Pid new_pid, const struct Program prog, + const struct Cred c); + + // Inform the tree of a process exit. + void HandleExit(uint64_t timestamp, const Process &p); + + // Mark the given pids as needing to be retained in the tree's map for future + // access. Normally, Processes are removed once all clients process past the + // event which would remove the Process (e.g. exit), however in cases where + // async processing occurs, the Process may need to be accessed after the + // exit. + void RetainProcess(std::vector &pids); + + // Release previously retained processes, signaling that the client is done + // processing the event that retained them. + void ReleaseProcess(std::vector &pids); + + // Annotate the given process with an Annotator (state). + void AnnotateProcess(const Process &p, std::shared_ptr a); + + // Get the given annotation on the given process if it exists, or nullopt if + // the annotation is not set. + template + std::optional> GetAnnotation(const Process &p) const; + + // Get the fully merged proto form of all annotations on the given process. + std::optional<::santa::pb::v1::process_tree::Annotations> ExportAnnotations(const struct Pid p); + + // Atomically get the slice of Processes going from the given process "up" + // to the root. The root process has no parent. N.B. There may be more than + // one root process. E.g. on Linux, both init (PID 1) and kthread (PID 2) + // are considered roots, as they are reported to have PPID=0. + std::vector> RootSlice( + std::shared_ptr p) const; + + // Call f for all processes in the tree. The list of processes is captured + // before invoking f, so it is safe to mutate the tree in f. + void Iterate(std::function)> f) const; + + // Get the Process for the given pid in the tree if it exists. + std::optional> Get( + const struct Pid target) const; + + // Traverse the tree from the given Process to its parent. + std::shared_ptr GetParent(const Process &p) const; + +#if SANTA_PROCESS_TREE_DEBUG + // Dump the tree in a human readable form to the given ostream. + void DebugDump(std::ostream &stream) const; +#endif + + private: + friend class ProcessTreeTestPeer; + void BackfillInsertChildren( + absl::flat_hash_map> &parent_map, + std::shared_ptr parent, const Process &unlinked_proc); + + // Mark that an event with the given timestamp is being processed. + // Returns whether the given timestamp is "novel", and the tree should be + // updated with the results of the event. + bool Step(uint64_t timestamp); + + std::optional> GetLocked( + const struct Pid target) const ABSL_SHARED_LOCKS_REQUIRED(mtx_); + + void DebugDumpLocked(std::ostream &stream, int depth, pid_t ppid) const; + + std::vector> annotators_; + + mutable absl::Mutex mtx_; + absl::flat_hash_map> map_ + ABSL_GUARDED_BY(mtx_); + // List of pids which should be removed from map_, and at the timestamp at + // which they should be. + // Elements are removed when the timestamp falls out of the seen_timestamps_ + // list below, signifying that all clients have synced past the timestamp. + std::vector> remove_at_ ABSL_GUARDED_BY(mtx_); + // Rolling list of event timestamps processed by the tree. + // This is used to ensure an event only gets processed once, even if events + // come out of order. + std::array seen_timestamps_ ABSL_GUARDED_BY(mtx_); +}; + +template +std::optional> ProcessTree::GetAnnotation( + const Process &p) const { + auto it = p.annotations_.find(std::type_index(typeid(T))); + if (it == p.annotations_.end()) { + return std::nullopt; + } + return std::dynamic_pointer_cast(it->second); +} + +// Create a new tree, ensuring the provided annotations are valid and that backfill +// is successful. +absl::StatusOr> CreateTree(std::vector> &&annotations); + +// ProcessTokens provide a lifetime based approach to retaining processes +// in a ProcessTree. When a token is created with a list of pids that may need +// to be referenced during processing of a given event, the ProcessToken informs +// the tree to retain those pids in its map so any call to ProcessTree::Get() +// during event processing succeeds. When the token is destroyed, it signals the +// tree to release the pids, which removes them from the tree if they would have +// fallen out otherwise due to a destruction event (e.g. exit). +class ProcessToken { + public: + explicit ProcessToken(std::shared_ptr tree, + std::vector pids); + ~ProcessToken(); + ProcessToken(const ProcessToken &other) + : ProcessToken(other.tree_, other.pids_) {} + ProcessToken(ProcessToken &&other) noexcept + : tree_(std::move(other.tree_)), pids_(std::move(other.pids_)) {} + ProcessToken &operator=(const ProcessToken &other) { + return *this = ProcessToken(other.tree_, other.pids_); + } + ProcessToken &operator=(ProcessToken &&other) noexcept { + tree_ = std::move(other.tree_); + pids_ = std::move(other.pids_); + return *this; + } + + private: + std::shared_ptr tree_; + std::vector pids_; +}; + +} // namespace santa::santad::process_tree + +#endif diff --git a/Source/santad/ProcessTree/process_tree.proto b/Source/santad/ProcessTree/process_tree.proto new file mode 100644 index 000000000..04b62a365 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +package santa.pb.v1.process_tree; + +message Annotations { +} diff --git a/Source/santad/ProcessTree/process_tree_macos.mm b/Source/santad/ProcessTree/process_tree_macos.mm new file mode 100644 index 000000000..b40f85831 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree_macos.mm @@ -0,0 +1,78 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#include "Source/santad/ProcessTree/process_tree.h" + +#include + +#include +#include + +#include "Source/santad/ProcessTree/process.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" + +namespace santa::santad::process_tree { + +absl::StatusOr LoadPID(pid_t pid) { + // TODO + return absl::UnimplementedError("LoadPID not implemented"); +} + +absl::Status ProcessTree::Backfill() { + int n_procs = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0); + if (n_procs < 0) { + return absl::InternalError("proc_listpids failed"); + } + n_procs /= sizeof(pid_t); + + std::vector pids; + pids.resize(n_procs + 16); // add space for a few more processes + // in case some spawn in-between. + + n_procs = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), (int)(pids.size() * sizeof(pid_t))); + if (n_procs < 0) { + return absl::InternalError("proc_listpids failed"); + } + n_procs /= sizeof(pid_t); + pids.resize(n_procs); + + absl::flat_hash_map> parent_map; + for (pid_t pid : pids) { + auto proc_status = LoadPID(pid); + if (proc_status.ok()) { + auto unlinked_proc = proc_status.value(); + + // Determine ppid + // Alternatively, there's a sysctl interface: + // https://chromium.googlesource.com/chromium/chromium/+/master/base/process_util_openbsd.cc#32 + struct proc_bsdinfo bsdinfo; + if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdinfo, sizeof(bsdinfo)) != + PROC_PIDTBSDINFO_SIZE) { + continue; + }; + + parent_map[bsdinfo.pbi_ppid].push_back(unlinked_proc); + } + } + + auto &roots = parent_map[0]; + for (const Process &p : roots) { + BackfillInsertChildren(parent_map, std::shared_ptr(), p); + } + + return absl::OkStatus(); +} + +} // namespace santa::santad::process_tree diff --git a/Source/santad/ProcessTree/process_tree_test.mm b/Source/santad/ProcessTree/process_tree_test.mm new file mode 100644 index 000000000..7f97bc4ad --- /dev/null +++ b/Source/santad/ProcessTree/process_tree_test.mm @@ -0,0 +1,220 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#import +#import + +#include + +#include +#include + +#include "Source/santad/ProcessTree/annotations/annotator.h" +#include "Source/santad/ProcessTree/process.h" +#include "Source/santad/ProcessTree/process_tree_test_helpers.h" +#include "absl/synchronization/mutex.h" + +namespace ptpb = ::santa::pb::v1::process_tree; + +namespace santa::santad::process_tree { + +static constexpr std::string_view kAnnotatedExecutable = "/usr/bin/login"; + +class TestAnnotator : public Annotator { + public: + TestAnnotator() {} + void AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) override; + void AnnotateExec(ProcessTree &tree, const Process &orig_process, + const Process &new_process) override; + std::optional<::ptpb::Annotations> Proto() const override; +}; + +void TestAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) { + // "Base case". Propagate existing annotations down to descendants. + if (auto annotation = tree.GetAnnotation(parent)) { + tree.AnnotateProcess(child, std::move(*annotation)); + } +} + +void TestAnnotator::AnnotateExec(ProcessTree &tree, const Process &orig_process, + const Process &new_process) { + if (auto annotation = tree.GetAnnotation(orig_process)) { + tree.AnnotateProcess(new_process, std::move(*annotation)); + return; + } + + if (new_process.program_->executable == kAnnotatedExecutable) { + tree.AnnotateProcess(new_process, std::make_shared()); + } +} + +std::optional<::ptpb::Annotations> TestAnnotator::Proto() const { + return std::nullopt; +} +} // namespace santa::santad::process_tree + +using namespace santa::santad::process_tree; + +@interface ProcessTreeTest : XCTestCase +@property std::shared_ptr tree; +@property std::shared_ptr init_proc; +@end + +@implementation ProcessTreeTest + +- (void)setUp { + std::vector> annotators{}; + self.tree = std::make_shared(std::move(annotators)); + self.init_proc = self.tree->InsertInit(); +} + +- (void)testSimpleOps { + uint64_t event_id = 1; + // PID 1.1: fork() -> PID 2.2 + const struct Pid child_pid = {.pid = 2, .pidversion = 2}; + self.tree->HandleFork(event_id++, *self.init_proc, child_pid); + + auto child_opt = self.tree->Get(child_pid); + XCTAssertTrue(child_opt.has_value()); + std::shared_ptr child = *child_opt; + XCTAssertEqual(child->pid_, child_pid); + XCTAssertEqual(child->program_, self.init_proc->program_); + XCTAssertEqual(child->effective_cred_, self.init_proc->effective_cred_); + XCTAssertEqual(self.tree->GetParent(*child), self.init_proc); + + // PID 2.2: exec("/bin/bash") -> PID 2.3 + const struct Pid child_exec_pid = {.pid = 2, .pidversion = 3}; + const struct Program child_exec_prog = {.executable = "/bin/bash", + .arguments = {"/bin/bash", "-i"}}; + self.tree->HandleExec(event_id++, *child, child_exec_pid, child_exec_prog, + child->effective_cred_); + + child_opt = self.tree->Get(child_exec_pid); + XCTAssertTrue(child_opt.has_value()); + child = *child_opt; + XCTAssertEqual(child->pid_, child_exec_pid); + XCTAssertEqual(*child->program_, child_exec_prog); + XCTAssertEqual(child->effective_cred_, self.init_proc->effective_cred_); +} + +- (void)testAnnotation { + std::vector> annotators{}; + annotators.emplace_back(std::make_unique()); + self.tree = std::make_shared(std::move(annotators)); + self.init_proc = self.tree->InsertInit(); + + uint64_t event_id = 1; + const struct Cred cred = {.uid = 0, .gid = 0}; + + // PID 1.1: fork() -> PID 2.2 + const struct Pid login_pid = {.pid = 2, .pidversion = 2}; + self.tree->HandleFork(event_id++, *self.init_proc, login_pid); + + // PID 2.2: exec("/usr/bin/login") -> PID 2.3 + const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3}; + const struct Program login_prog = {.executable = std::string(kAnnotatedExecutable), + .arguments = {}}; + auto login = *self.tree->Get(login_pid); + self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred); + + // Ensure we have an annotation on login itself... + login = *self.tree->Get(login_exec_pid); + auto annotation = self.tree->GetAnnotation(*login); + XCTAssertTrue(annotation.has_value()); + + // PID 2.3: fork() -> PID 3.3 + const struct Pid shell_pid = {.pid = 3, .pidversion = 3}; + self.tree->HandleFork(event_id++, *login, shell_pid); + // PID 3.3: exec("/bin/zsh") -> PID 3.4 + const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4}; + const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}}; + auto shell = *self.tree->Get(shell_pid); + self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred); + + // ... and also ensure we have an annotation on the descendant zsh. + shell = *self.tree->Get(shell_exec_pid); + annotation = self.tree->GetAnnotation(*shell); + XCTAssertTrue(annotation.has_value()); +} + +- (void)testCleanup { + uint64_t event_id = 1; + const struct Pid child_pid = {.pid = 2, .pidversion = 2}; + { + self.tree->HandleFork(event_id++, *self.init_proc, child_pid); + auto child = *self.tree->Get(child_pid); + self.tree->HandleExit(event_id++, *child); + } + + // We should still be able to get a handle to child... + { + auto child = self.tree->Get(child_pid); + XCTAssertTrue(child.has_value()); + } + + // ... until we step far enough into the future (32 events). + struct Pid churn_pid = {.pid = 3, .pidversion = 3}; + for (int i = 0; i < 32; i++) { + self.tree->HandleFork(event_id++, *self.init_proc, churn_pid); + churn_pid.pid++; + } + + // Now when we try processing the next event, it should have fallen out of the tree. + self.tree->HandleFork(event_id++, *self.init_proc, churn_pid); + { + auto child = self.tree->Get(child_pid); + XCTAssertFalse(child.has_value()); + } +} + +- (void)testRefcountCleanup { + uint64_t event_id = 1; + const struct Pid child_pid = {.pid = 2, .pidversion = 2}; + { + self.tree->HandleFork(event_id++, *self.init_proc, child_pid); + auto child = *self.tree->Get(child_pid); + self.tree->HandleExit(event_id++, *child); + } + + { + auto child = self.tree->Get(child_pid); + XCTAssertTrue(child.has_value()); + std::vector pids = {(*child)->pid_}; + self.tree->RetainProcess(pids); + } + + // Even if we step far into the future, we should still be able to lookup + // the child. + for (int i = 0; i < 1000; i++) { + struct Pid churn_pid = {.pid = 100 + i, .pidversion = (uint64_t)(100 + i)}; + self.tree->HandleFork(event_id++, *self.init_proc, churn_pid); + auto child = self.tree->Get(child_pid); + XCTAssertTrue(child.has_value()); + } + + // But when released... + { + auto child = self.tree->Get(child_pid); + XCTAssertTrue(child.has_value()); + std::vector pids = {(*child)->pid_}; + self.tree->ReleaseProcess(pids); + } + + // ... it should immediately be removed. + { + auto child = self.tree->Get(child_pid); + XCTAssertFalse(child.has_value()); + } +} + +@end diff --git a/Source/santad/ProcessTree/process_tree_test_helpers.h b/Source/santad/ProcessTree/process_tree_test_helpers.h new file mode 100644 index 000000000..6bf5209d7 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree_test_helpers.h @@ -0,0 +1,30 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#ifndef SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H +#define SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H +#include + +#include "Source/santad/ProcessTree/process_tree.h" + +namespace santa::santad::process_tree { + +class ProcessTreeTestPeer : public ProcessTree { + public: + explicit ProcessTreeTestPeer(std::vector> &&annotators) : ProcessTree(std::move(annotators)) {} + std::shared_ptr InsertInit(); +}; + +} // namespace santa::santad::process_tree + +#endif diff --git a/Source/santad/ProcessTree/process_tree_test_helpers.mm b/Source/santad/ProcessTree/process_tree_test_helpers.mm new file mode 100644 index 000000000..6bc4f3752 --- /dev/null +++ b/Source/santad/ProcessTree/process_tree_test_helpers.mm @@ -0,0 +1,42 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#import + +#include +#include + +#include "Source/santad/ProcessTree/process.h" +#include "Source/santad/ProcessTree/process_tree.h" + +namespace santa::santad::process_tree { + +class ProcessTreeTestPeer : public ProcessTree { + public: + std::shared_ptr InsertInit(); +}; + +std::shared_ptr ProcessTreeTestPeer::InsertInit() { + absl::MutexLock lock(&mtx_); + struct Pid initpid = { + .pid = 1, + .pidversion = 1, + }; + auto proc = std::make_shared( + initpid, (Cred){.uid = 0, .gid = 0}, + std::make_shared((Program){.executable = "/init", .arguments = {"/init"}}), nullptr); + map_.emplace(initpid, proc); + return proc; +} + +} // namespace santa::santad::process_tree