usb_bouncer: Daemonize and cleanup udev commands.

This eliminates some of the cases where usb_bouncer blocks udev for a
relatively long time, and completely removes waiting on syslog or D-Bus
since the waiting needs to happen before entering the jail.

BUG=chromium:1133849, b:170306064
TEST=FEATURES=test emerge-${BOARD} usb_bouncer &&
  tast run ${TEST_DEVICE} security.USBBouncer.check_seccomp

Change-Id: I3da38c55d8038f0708a7c4fc461322e49cae3217
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2466610
Tested-by: Allen Webb <allenwebb@google.com>
Reviewed-by: Jorge Lucangeli Obes <jorgelo@chromium.org>
Commit-Queue: Allen Webb <allenwebb@google.com>
(cherry picked from commit 2a0a5e54c5dba3ee0d193a6e9546ab17e1fdeb59)
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2581747
Reviewed-by: Allen Webb <allenwebb@google.com>
Commit-Queue: Henry Sun <henrysun@google.com>
Tested-by: Henry Sun <henrysun@google.com>
diff --git a/usb_bouncer/40-usb-bouncer.rules b/usb_bouncer/40-usb-bouncer.rules
index 19daa63..c76fab5 100644
--- a/usb_bouncer/40-usb-bouncer.rules
+++ b/usb_bouncer/40-usb-bouncer.rules
@@ -1,3 +1,3 @@
 # Keep track of USB devices that should be allow-listed at the lock screen.
-ACTION=="add", DRIVER=="usb", RUN+="/usr/sbin/usb_bouncer udev add '%E{DEVPATH}'"
-ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="/usr/sbin/usb_bouncer udev remove '%E{DEVPATH}'"
+ACTION=="add", DRIVER=="usb", RUN+="/usr/sbin/usb_bouncer --fork udev add '%E{DEVPATH}'"
+ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="/usr/sbin/usb_bouncer --fork udev remove '%E{DEVPATH}'"
diff --git a/usb_bouncer/entry_manager.cc b/usb_bouncer/entry_manager.cc
index 1ebfb4b..d87afee 100644
--- a/usb_bouncer/entry_manager.cc
+++ b/usb_bouncer/entry_manager.cc
@@ -31,8 +31,10 @@
 
 }  // namespace
 
-EntryManager* EntryManager::GetInstance() {
-  static EntryManager instance;
+EntryManager* EntryManager::GetInstance(
+    DevpathToRuleCallback rule_from_devpath) {
+  static EntryManager instance("/", GetUserDBDir(), IsLockscreenShown(),
+                               IsGuestSession(), rule_from_devpath);
   if (!instance.global_db_.Valid()) {
     LOG(ERROR) << "Failed to open global DB.";
     return nullptr;
diff --git a/usb_bouncer/entry_manager.h b/usb_bouncer/entry_manager.h
index 539dd68..3aae235 100644
--- a/usb_bouncer/entry_manager.h
+++ b/usb_bouncer/entry_manager.h
@@ -41,7 +41,7 @@
  public:
   enum class UdevAction { kAdd = 0, kRemove = 1 };
 
-  static EntryManager* GetInstance();
+  static EntryManager* GetInstance(DevpathToRuleCallback rule_from_devpath);
   static bool CreateDefaultGlobalDB();
 
   ~EntryManager();
diff --git a/usb_bouncer/seccomp/usb_bouncer-seccomp-amd64.policy b/usb_bouncer/seccomp/usb_bouncer-seccomp-amd64.policy
index 8e8ddcf..8769aa8 100644
--- a/usb_bouncer/seccomp/usb_bouncer-seccomp-amd64.policy
+++ b/usb_bouncer/seccomp/usb_bouncer-seccomp-amd64.policy
@@ -39,12 +39,15 @@
 
 # These were not included by security.USBBouncer.
 clock_gettime: 1
+clone: 1
 creat: 1
+dup3: 1
 fchown: 1
 newfstatat: 1
 futex: 1
 getpid: 1
 gettimeofday: 1
+ioctl: arg1 == TCGETS
 lstat: 1
 mmap: arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE
 mprotect: arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE
@@ -53,5 +56,6 @@
 rt_sigprocmask: 1
 set_robust_list: 1
 set_tid_address: 1
+setsid: 1
 tgkill: 1
 unlinkat: 1
diff --git a/usb_bouncer/seccomp/usb_bouncer-seccomp-arm.policy b/usb_bouncer/seccomp/usb_bouncer-seccomp-arm.policy
index 4b29148..99a709b 100644
--- a/usb_bouncer/seccomp/usb_bouncer-seccomp-arm.policy
+++ b/usb_bouncer/seccomp/usb_bouncer-seccomp-arm.policy
@@ -43,8 +43,11 @@
 
 # These were not included by security.USBBouncer.
 ARM_set_tls: 1
+clone: 1
+dup3: 1
 fstatat64: 1
 getpid: 1
+ioctl: arg1 == TCGETS
 lstat64: 1
 mmap2: arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE
 mprotect: arg2 in ~PROT_EXEC || arg2 in ~PROT_WRITE
@@ -53,6 +56,7 @@
 rt_sigprocmask: 1
 set_robust_list: 1
 set_tid_address: 1
+setsid: 1
 tgkill: 1
 ugetrlimit: 1
 unlinkat: 1
diff --git a/usb_bouncer/seccomp/usb_bouncer-seccomp-arm64.policy b/usb_bouncer/seccomp/usb_bouncer-seccomp-arm64.policy
index 75be97d..2d500cd 100644
--- a/usb_bouncer/seccomp/usb_bouncer-seccomp-arm64.policy
+++ b/usb_bouncer/seccomp/usb_bouncer-seccomp-arm64.policy
@@ -41,6 +41,8 @@
 # These were not included by security.USBBouncer.
 clock_getres: 1
 clock_gettime: 1
+clone: 1
+dup3: 1
 getpid: 1
 gettimeofday: 1
 ioctl: arg1 == TCGETS
@@ -51,5 +53,6 @@
 rt_sigprocmask: 1
 set_robust_list: 1
 set_tid_address: 1
+setsid: 1
 tgkill: 1
 unlinkat: 1
diff --git a/usb_bouncer/usb_bouncer.cc b/usb_bouncer/usb_bouncer.cc
index 578da33..3068c50 100644
--- a/usb_bouncer/usb_bouncer.cc
+++ b/usb_bouncer/usb_bouncer.cc
@@ -7,7 +7,6 @@
 #include <cstdio>
 #include <cstdlib>
 
-#include <base/bind.h>
 #include <base/command_line.h>
 #include <brillo/syslog_logging.h>
 #include <brillo/flag_helper.h>
@@ -17,8 +16,10 @@
 #include "usb_bouncer/entry_manager.h"
 #include "usb_bouncer/util.h"
 
+using usb_bouncer::Daemonize;
+using usb_bouncer::DevpathToRuleCallback;
 using usb_bouncer::EntryManager;
-using usb_bouncer::ForkAndWaitIfNotReady;
+using usb_bouncer::GetRuleFromDevPath;
 using usb_bouncer::kDBusPath;
 
 namespace {
@@ -35,16 +36,27 @@
   kDisabled,
 };
 
+enum class ForkConfig {
+  kDoubleFork,
+  kDisabled,
+};
+
+class Configuration {
+ public:
+  const SeccompEnforcement seccomp;
+  const ForkConfig fork_config;
+};
+
 static constexpr char kLogPath[] = "/dev/log";
 static constexpr char kUMAEventsPath[] = "/var/lib/metrics/uma-events";
 
-void DropPrivileges(SeccompEnforcement seccomp) {
+void DropPrivileges(const Configuration& config) {
   ScopedMinijail j(minijail_new());
   minijail_change_user(j.get(), usb_bouncer::kUsbBouncerUser);
   minijail_change_group(j.get(), usb_bouncer::kUsbBouncerGroup);
   minijail_inherit_usergroups(j.get());
   minijail_no_new_privs(j.get());
-  if (seccomp == SeccompEnforcement::kEnabled) {
+  if (config.seccomp == SeccompEnforcement::kEnabled) {
     minijail_use_seccomp_filter(j.get());
     minijail_parse_seccomp_filters(
         j.get(), "/usr/share/policy/usb_bouncer-seccomp.policy");
@@ -52,7 +64,13 @@
 
   minijail_namespace_ipc(j.get());
   minijail_namespace_net(j.get());
-  minijail_namespace_pids(j.get());
+  // If minijail were to run as init, then it would be tracked by udev and
+  // defeat the purpose of daemonizing. If minijail doesn't run as init, the
+  // descendant processes will die when daemonizing because there won't be an
+  // init to keep the pid namespace from closing.
+  if (config.fork_config == ForkConfig::kDisabled) {
+    minijail_namespace_pids(j.get());
+  }
   minijail_namespace_uts(j.get());
   minijail_namespace_vfs(j.get());
   if (minijail_enter_pivot_root(j.get(), "/mnt/empty") != 0) {
@@ -122,19 +140,19 @@
   umask(0077);
 }
 
-EntryManager* GetEntryManagerOrDie(SeccompEnforcement seccomp) {
+EntryManager* GetEntryManagerOrDie(const Configuration& config) {
   if (!EntryManager::CreateDefaultGlobalDB()) {
     LOG(FATAL) << "Unable to create default global DB!";
   }
-  DropPrivileges(seccomp);
-  EntryManager* entry_manager = EntryManager::GetInstance();
+  DropPrivileges(config);
+  EntryManager* entry_manager = EntryManager::GetInstance(GetRuleFromDevPath);
   if (!entry_manager) {
     LOG(FATAL) << "EntryManager::GetInstance() failed!";
   }
   return entry_manager;
 }
 
-int HandleAuthorizeAll(SeccompEnforcement seccomp,
+int HandleAuthorizeAll(const Configuration& config,
                        const std::vector<std::string>& argv) {
   if (!argv.empty()) {
     LOG(ERROR) << "Invalid options!";
@@ -148,14 +166,14 @@
   return EXIT_SUCCESS;
 }
 
-int HandleCleanup(SeccompEnforcement seccomp,
+int HandleCleanup(const Configuration& config,
                   const std::vector<std::string>& argv) {
   if (!argv.empty()) {
     LOG(ERROR) << "Invalid options!";
     return EXIT_FAILURE;
   }
 
-  EntryManager* entry_manager = GetEntryManagerOrDie(seccomp);
+  EntryManager* entry_manager = GetEntryManagerOrDie(config);
   if (!entry_manager->GarbageCollect()) {
     LOG(ERROR) << "cleanup failed!";
     return EXIT_FAILURE;
@@ -163,14 +181,14 @@
   return EXIT_SUCCESS;
 }
 
-int HandleGenRules(SeccompEnforcement seccomp,
+int HandleGenRules(const Configuration& config,
                    const std::vector<std::string>& argv) {
   if (!argv.empty()) {
     LOG(ERROR) << "Invalid options!";
     return EXIT_FAILURE;
   }
 
-  EntryManager* entry_manager = GetEntryManagerOrDie(seccomp);
+  EntryManager* entry_manager = GetEntryManagerOrDie(config);
   std::string rules = entry_manager->GenerateRules();
   if (rules.empty()) {
     LOG(ERROR) << "genrules failed!";
@@ -181,7 +199,7 @@
   return EXIT_SUCCESS;
 }
 
-int HandleUdev(SeccompEnforcement seccomp,
+int HandleUdev(const Configuration& config,
                const std::vector<std::string>& argv) {
   if (argv.size() != 2) {
     LOG(ERROR) << "Invalid options!";
@@ -198,30 +216,61 @@
     return EXIT_FAILURE;
   }
 
-  // The return code isn't checked because LOG statements are already present in
-  // the function. The return value allows tests to validate the behavior.
-  ForkAndWaitIfNotReady(base::BindRepeating([]() -> bool {
-                          return base::PathExists(base::FilePath(kLogPath)) &&
-                                 base::PathExists(base::FilePath(kDBusPath));
-                        }),
-                        "logging or D-Bus isn't ready");
+  // We need to drop privileges prior to reading from sysfs so instead of
+  // calling GetEntryManagerOrDie split it up so the privileges can be dropped
+  // earlier.
+  if (!EntryManager::CreateDefaultGlobalDB()) {
+    LOG(FATAL) << "Unable to create default global DB!";
+  }
+  DropPrivileges(config);
 
-  EntryManager* entry_manager = GetEntryManagerOrDie(seccomp);
-  if (!entry_manager->HandleUdev(action, argv[1])) {
+  // Perform sysfs reads before daemonizing to avoid races.
+  const std::string& devpath = argv[1];
+  std::string rule;
+  if (action == EntryManager::UdevAction::kAdd) {
+    rule = GetRuleFromDevPath(devpath);
+    if (rule.empty()) {
+      LOG(ERROR) << "Unable convert devpath to USBGuard allow-list rule.";
+      exit(0);
+    }
+  }
+
+  // All the information needed from udev and sysfs should be obtained prior to
+  // this point. Daemonizing here allows usb_bouncer to wait on other system
+  // services without blocking udev.
+  if (config.fork_config == ForkConfig::kDoubleFork) {
+    Daemonize();
+  }
+
+  // The DevpathToRuleCallback here to forwards the result of the sysfs read
+  // performed before daemonizing.
+  EntryManager* entry_manager = EntryManager::GetInstance(
+      [rule, devpath](const std::string& devpath_) -> const std::string {
+        if (devpath != devpath_) {
+          LOG(ERROR) << "Got devpath: '" << devpath_ << "' expected '"
+                     << devpath;
+          return "";
+        }
+        return rule;
+      });
+  if (!entry_manager) {
+    LOG(FATAL) << "EntryManager::GetInstance() failed!";
+  }
+  if (!entry_manager->HandleUdev(action, devpath)) {
     LOG(ERROR) << "udev failed!";
     return EXIT_FAILURE;
   }
   return EXIT_SUCCESS;
 }
 
-int HandleUserLogin(SeccompEnforcement seccomp,
+int HandleUserLogin(const Configuration& config,
                     const std::vector<std::string>& argv) {
   if (!argv.empty()) {
     LOG(ERROR) << "Invalid options!";
     return EXIT_FAILURE;
   }
 
-  EntryManager* entry_manager = GetEntryManagerOrDie(seccomp);
+  EntryManager* entry_manager = GetEntryManagerOrDie(config);
   if (!entry_manager->HandleUserLogin()) {
     LOG(ERROR) << "userlogin failed!";
     return EXIT_FAILURE;
@@ -237,6 +286,8 @@
       DCHECK_IS_ON()
           ? "Enable or disable seccomp filtering."
           : "Flag is ignored in production, but reported as a crash if false.");
+  DEFINE_bool(fork, false,
+              "Daemonizes udev commands with a double fork if enabled.");
   brillo::FlagHelper::Init(argc, argv, kUsageMessage);
   base::CommandLine::Init(argc, argv);
 
@@ -289,10 +340,12 @@
   } else {
     seccomp = SeccompEnforcement::kEnabled;
   }
+  ForkConfig fork_config =
+      FLAGS_fork ? ForkConfig::kDoubleFork : ForkConfig::kDisabled;
 
   const struct {
     const std::string command;
-    int (*handler)(SeccompEnforcement seccomp,
+    int (*handler)(const Configuration& config,
                    const std::vector<std::string>& argv);
   } command_handlers[] = {
       // clang-format off
@@ -307,7 +360,8 @@
   for (const auto& command_handler : command_handlers) {
     if (command_handler.command == command) {
       return command_handler.handler(
-          seccomp, std::vector<std::string>(command_args_start, args.end()));
+          {seccomp, fork_config},
+          std::vector<std::string>(command_args_start, args.end()));
     }
   }
 
diff --git a/usb_bouncer/util.cc b/usb_bouncer/util.cc
index e7b105a..25fe6f8 100644
--- a/usb_bouncer/util.cc
+++ b/usb_bouncer/util.cc
@@ -601,48 +601,38 @@
   return num_removed;
 }
 
-pid_t DoubleFork() {
-  if (fork() != 0) {
+void Daemonize() {
+  pid_t result = fork();
+  if (result < 0) {
+    PLOG(FATAL) << "First fork failed";
+  }
+  if (result != 0) {
     exit(0);
   }
 
   setsid();
 
-  return fork();
-}
-
-bool ForkAndWaitIfNotReady(const base::RepeatingCallback<bool()> ready,
-                           const std::string message,
-                           const base::TimeDelta& timeout,
-                           base::Callback<pid_t()> fork_func) {
-  if (ready.Run()) {
-    return true;
+  result = fork();
+  if (result < 0) {
+    PLOG(FATAL) << "Second fork failed";
   }
-
-  // Exit success for the parent to allow udev to continue but fork so the event
-  // can be handled once logging is available.
-  if (fork_func.Run() != 0) {
+  if (result != 0) {
     exit(0);
   }
 
-  if (ready.Run()) {
-    LOG(INFO) << "Forked because " << message;
-    return true;
+  // Since we're demonizing we don't expect to ever read or write from the
+  // standard file descriptors. Also, udev waits for the hangup before
+  // continuing to execute on the same event, so this is necessary to unblock
+  // udev.
+  if (freopen("/dev/null", "a+", stdout) == nullptr) {
+    LOG(FATAL) << "Failed to replace stdout.";
   }
-
-  const base::Time deadline = base::Time::Now() + timeout;
-  const base::TimeDelta check_interval = base::TimeDelta::FromMilliseconds(250);
-
-  while (base::Time::Now() < deadline) {
-    base::PlatformThread::Sleep(check_interval);
-    if (ready.Run()) {
-      LOG(INFO) << "Forked because " << message;
-      return true;
-    }
+  if (freopen("/dev/null", "a+", stderr) == nullptr) {
+    LOG(FATAL) << "Failed to replace stdout.";
   }
-
-  LOG(ERROR) << "Timed out after forking because " << message;
-  return false;
+  if (fclose(stdin) != 0) {
+    LOG(FATAL) << "Failed to close stdin.";
+  }
 }
 
 #define TO_STRING_HELPER(x)  \
diff --git a/usb_bouncer/util.h b/usb_bouncer/util.h
index 669c282..09b2556 100644
--- a/usb_bouncer/util.h
+++ b/usb_bouncer/util.h
@@ -119,23 +119,13 @@
                              const std::string& state_file_name,
                              bool lock);
 
-// This implements a double fork with setsid for use with ForkAndWaitIfNotReady.
-pid_t DoubleFork();
-
-// Returns true if |ready| returns true. If it returns true immediately, no
-// further action is taken. Otherwise, the process is forked and the parent
-// exits immediately. The child will wait until |ready| returns true or the
-// |timeout| is reached. |message| is printed to the log as the reason for
-// forking the process.
+// Forks (exiting the parent), calls setsid, and returns the result of a second
+// fork.
 //
 // This is used to avoid blocking udev while waiting on journald to finish
-// setting up logging or D-Bus to be ready. |fork_func| is provided for
-// testability (note that if fork_func returns non-zero, exit(0) is called).
-bool ForkAndWaitIfNotReady(
-    const base::RepeatingCallback<bool()> ready,
-    const std::string message,
-    const base::TimeDelta& timeout = base::TimeDelta::FromSeconds(5),
-    base::Callback<pid_t()> fork_func = base::Bind(&DoubleFork));
+// setting up logging, D-Bus to be ready, or D-Bus calls that can take on the
+// order of seconds to complete.
+void Daemonize();
 
 void UpdateTimestamp(Timestamp* timestamp);
 size_t RemoveEntriesOlderThan(base::TimeDelta cutoff, EntryMap* map);
diff --git a/usb_bouncer/util_test.cc b/usb_bouncer/util_test.cc
index ed466e9..ec2b2a0 100644
--- a/usb_bouncer/util_test.cc
+++ b/usb_bouncer/util_test.cc
@@ -52,21 +52,6 @@
          CheckEnabledFlag(dir.Append(kSysFSAuthorizedDefault));
 }
 
-// Mock for testing ForkAndWaitIfDoesNotExist(...). It sets the |taken| to true,
-// and always returns 0. If |to_create| isn't empty, it creates a directory at
-// that path to make it possible to test the case the path is created after the
-// fork.
-int ForkMock(bool* taken, const base::FilePath& to_create) {
-  *taken = true;
-
-  if (!to_create.empty()) {
-    base::CreateDirectory(to_create);
-  }
-
-  // Always take the child path, because the parent path exits.
-  return 0;
-}
-
 }  // namespace
 
 TEST(UtilTest, IncludeRuleAtLockscreen) {
@@ -124,51 +109,6 @@
   EXPECT_TRUE(CheckDeviceNodeAuthorized(deepdir));
 }
 
-TEST(UtilTest, ForkAndWaitIfNotReady_Ready) {
-  base::FilePath empty_path;
-  base::ScopedTempDir temp_dir_;
-  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()) << strerror(errno);
-  const base::FilePath temp_dir_path = temp_dir_.GetPath();
-
-  bool fork_taken = false;
-  EXPECT_TRUE(ForkAndWaitIfNotReady(
-      base::BindLambdaForTesting(
-          [&temp_dir_path]() -> bool { return PathExists(temp_dir_path); }),
-      "temp_dir doesn't exist", base::TimeDelta::FromSeconds(0),
-      base::Bind(&ForkMock, base::Unretained(&fork_taken), empty_path)));
-  EXPECT_FALSE(fork_taken);
-}
-
-TEST(UtilTest, ForkAndWaitIfPathUnavailable_PathDoesntExistTimeout) {
-  base::FilePath empty_path;
-  base::ScopedTempDir temp_dir_;
-  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()) << strerror(errno);
-  base::FilePath sub_dir_ = temp_dir_.GetPath().Append("DoesNotExist");
-  LOG(INFO) << "PATH: " << sub_dir_.value();
-
-  bool fork_taken = false;
-  EXPECT_FALSE(ForkAndWaitIfNotReady(
-      base::BindLambdaForTesting(
-          [&sub_dir_]() -> bool { return PathExists(sub_dir_); }),
-      "sub_dir_ doesn't exist", base::TimeDelta::FromSeconds(0),
-      base::Bind(&ForkMock, base::Unretained(&fork_taken), empty_path)));
-  EXPECT_TRUE(fork_taken);
-}
-
-TEST(UtilTest, ForkAndWaitIfPathUnavailable_PathDoesntExistSucceed) {
-  base::ScopedTempDir temp_dir_;
-  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()) << strerror(errno);
-  base::FilePath sub_dir_ = temp_dir_.GetPath().Append("ExistsAfterFork");
-
-  bool fork_taken = false;
-  EXPECT_TRUE(ForkAndWaitIfNotReady(
-      base::BindLambdaForTesting(
-          [&sub_dir_]() -> bool { return PathExists(sub_dir_); }),
-      "sub_dir_ doesn't exist", base::TimeDelta::FromSeconds(1),
-      base::Bind(&ForkMock, base::Unretained(&fork_taken), sub_dir_)));
-  EXPECT_TRUE(fork_taken);
-}
-
 TEST(UtilTest, GetRuleFromString_Success) {
   EXPECT_TRUE(GetRuleFromString("allow id 0000:0000"));
 }