cros-disks: Make a mounter for archives and remove legacy mounter

Move archive-specific bits of the legacy mounter into a dedicated
archive mounter.

BUG=chromium:950442
BUG=chromium:933018
TEST=platform.CrosDisks*

Change-Id: I852a95c7de3ed656e275e5447ef821e780e04ab6
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2580960
Tested-by: Sergei Datsenko <dats@chromium.org>
Reviewed-by: Austin Tankiang <austinct@chromium.org>
Commit-Queue: Sergei Datsenko <dats@chromium.org>
Auto-Submit: Sergei Datsenko <dats@chromium.org>
diff --git a/cros-disks/BUILD.gn b/cros-disks/BUILD.gn
index 35fcd86..64377f4 100644
--- a/cros-disks/BUILD.gn
+++ b/cros-disks/BUILD.gn
@@ -43,6 +43,7 @@
 static_library("libdisks") {
   sources = [
     "archive_manager.cc",
+    "archive_mounter.cc",
     "cros_disks_server.cc",
     "daemon.cc",
     "device_ejector.cc",
@@ -124,6 +125,7 @@
   executable("disks_testrunner") {
     sources = [
       "archive_manager_test.cc",
+      "archive_mounter_test.cc",
       "device_event_moderator_test.cc",
       "device_event_queue_test.cc",
       "disk_manager_test.cc",
diff --git a/cros-disks/archive_mounter.cc b/cros-disks/archive_mounter.cc
new file mode 100644
index 0000000..d4bf17a
--- /dev/null
+++ b/cros-disks/archive_mounter.cc
@@ -0,0 +1,154 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "cros-disks/archive_mounter.h"
+
+#include <utility>
+
+#include <base/strings/stringprintf.h>
+#include <base/strings/string_util.h>
+#include <brillo/scoped_mount_namespace.h>
+
+#include "cros-disks/quote.h"
+
+namespace cros_disks {
+
+namespace {
+constexpr char kOptionPassword[] = "password";
+}  // namespace
+
+ArchiveMounter::ArchiveMounter(
+    const Platform* platform,
+    brillo::ProcessReaper* process_reaper,
+    std::string archive_type,
+    Metrics* metrics,
+    std::string metrics_name,
+    std::vector<int> password_needed_exit_codes,
+    std::unique_ptr<SandboxedProcessFactory> sandbox_factory)
+    : FUSEMounter(
+          platform, process_reaper, archive_type + "fs", {.read_only = true}),
+      archive_type_(archive_type),
+      extension_("." + archive_type),
+      metrics_(metrics),
+      metrics_name_(std::move(metrics_name)),
+      password_needed_exit_codes_(std::move(password_needed_exit_codes)),
+      sandbox_factory_(std::move(sandbox_factory)) {}
+
+ArchiveMounter::~ArchiveMounter() = default;
+
+bool ArchiveMounter::CanMount(const std::string& source,
+                              const std::vector<std::string>& /*params*/,
+                              base::FilePath* suggested_dir_name) const {
+  base::FilePath path(source);
+  if (path.IsAbsolute() &&
+      base::CompareCaseInsensitiveASCII(path.Extension(), extension_) == 0) {
+    *suggested_dir_name = path.BaseName();
+    return true;
+  }
+  return false;
+}
+
+MountErrorType ArchiveMounter::InterpretReturnCode(int return_code) const {
+  if (metrics_ && !metrics_name_.empty())
+    metrics_->RecordFuseMounterErrorCode(metrics_name_, return_code);
+
+  if (base::Contains(password_needed_exit_codes_, return_code))
+    return MOUNT_ERROR_NEED_PASSWORD;
+  return FUSEMounter::InterpretReturnCode(return_code);
+}
+
+std::unique_ptr<SandboxedProcess> ArchiveMounter::PrepareSandbox(
+    const std::string& source,
+    const base::FilePath& /*target_path*/,
+    std::vector<std::string> params,
+    MountErrorType* error) const {
+  metrics_->RecordArchiveType(archive_type_);
+
+  base::FilePath path(source);
+  if (!path.IsAbsolute() || path.ReferencesParent()) {
+    LOG(ERROR) << "Invalid archive path " << quote(path);
+    *error = MOUNT_ERROR_INVALID_ARGUMENT;
+    return nullptr;
+  }
+
+  auto sandbox = sandbox_factory_->CreateSandboxedProcess();
+
+  std::unique_ptr<brillo::ScopedMountNamespace> mount_ns;
+  if (!platform()->PathExists(path.value())) {
+    // Try to locate the file in Chrome's mount namespace.
+    mount_ns = brillo::ScopedMountNamespace::CreateFromPath(
+        base::FilePath(kChromeNamespace));
+    if (!mount_ns) {
+      PLOG(ERROR) << "Could not look for archive " << quote(path)
+                  << " in the Chrome's namespace";
+      // TODO(dats): These probably should be MOUNT_ERROR_INVALID_DEVICE_PATH or
+      //             something like that, but tast tests expect
+      //             MOUNT_ERROR_MOUNT_PROGRAM_FAILED.
+      *error = MOUNT_ERROR_MOUNT_PROGRAM_FAILED;
+      return nullptr;
+    }
+    if (!platform()->PathExists(path.value())) {
+      PLOG(ERROR) << "Could not find archive " << quote(path);
+      *error = MOUNT_ERROR_MOUNT_PROGRAM_FAILED;
+      return nullptr;
+    }
+  }
+
+  // Archives are typically under /home, /media or /run. To bind-mount the
+  // source those directories must be writable, but by default only /run is.
+  for (const char* const dir : {"/home", "/media"}) {
+    if (!sandbox->Mount("tmpfs", dir, "tmpfs", "mode=0755,size=1M")) {
+      LOG(ERROR) << "Cannot mount " << quote(dir);
+      *error = MOUNT_ERROR_INTERNAL;
+      return nullptr;
+    }
+  }
+
+  // Is the process "password-aware"?
+  if (!password_needed_exit_codes_.empty()) {
+    std::string password;
+    if (GetParamValue(params, kOptionPassword, &password)) {
+      sandbox->SetStdIn(password);
+    }
+  }
+
+  *error = FormatInvocationCommand(path, std::move(params), sandbox.get());
+  if (*error != MOUNT_ERROR_NONE) {
+    return nullptr;
+  }
+
+  if (mount_ns) {
+    // Sandbox will need to enter Chrome's namespace too to access files.
+    mount_ns.reset();
+    sandbox->EnterExistingMountNamespace(kChromeNamespace);
+  }
+
+  return sandbox;
+}
+
+MountErrorType ArchiveMounter::FormatInvocationCommand(
+    const base::FilePath& archive,
+    std::vector<std::string> /*params*/,
+    SandboxedProcess* sandbox) const {
+  // Make the source available in the sandbox.
+  if (!sandbox->BindMount(archive.value(), archive.value(),
+                          /* writeable= */ false,
+                          /* recursive= */ false)) {
+    LOG(ERROR) << "Cannot bind the source archive " << quote(archive);
+    return MOUNT_ERROR_INTERNAL;
+  }
+
+  std::vector<std::string> opts = {
+      MountOptions::kOptionReadOnly, "umask=0222",
+      base::StringPrintf("uid=%d", kChronosUID),
+      base::StringPrintf("gid=%d", kChronosAccessGID)};
+
+  sandbox->AddArgument("-o");
+  sandbox->AddArgument(base::JoinString(opts, ","));
+  sandbox->AddArgument(archive.value());
+
+  return MOUNT_ERROR_NONE;
+}
+
+}  // namespace cros_disks
diff --git a/cros-disks/archive_mounter.h b/cros-disks/archive_mounter.h
new file mode 100644
index 0000000..2c03cb6
--- /dev/null
+++ b/cros-disks/archive_mounter.h
@@ -0,0 +1,67 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CROS_DISKS_ARCHIVE_MOUNTER_H_
+#define CROS_DISKS_ARCHIVE_MOUNTER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "cros-disks/fuse_mounter.h"
+
+namespace cros_disks {
+
+class Metrics;
+
+// An implementation of FUSEMounter tailored for mounting archives.
+class ArchiveMounter : public FUSEMounter {
+ public:
+  static constexpr char kChromeNamespace[] = "/run/namespaces/mnt_chrome";
+
+  ArchiveMounter(const Platform* platform,
+                 brillo::ProcessReaper* process_reaper,
+                 std::string archive_type,
+                 Metrics* metrics,
+                 std::string metrics_name,
+                 std::vector<int> password_needed_exit_codes,
+                 std::unique_ptr<SandboxedProcessFactory> sandbox_factory);
+  ArchiveMounter(const ArchiveMounter&) = delete;
+  ArchiveMounter& operator=(const ArchiveMounter&) = delete;
+
+  ~ArchiveMounter() override;
+
+  bool CanMount(const std::string& source,
+                const std::vector<std::string>& params,
+                base::FilePath* suggested_dir_name) const override;
+
+ protected:
+  // FUSEMounter overrides:
+  MountErrorType InterpretReturnCode(int return_code) const override;
+
+  std::unique_ptr<SandboxedProcess> PrepareSandbox(
+      const std::string& source,
+      const base::FilePath& target_path,
+      std::vector<std::string> params,
+      MountErrorType* error) const final;
+
+  virtual MountErrorType FormatInvocationCommand(
+      const base::FilePath& archive,
+      std::vector<std::string> params,
+      SandboxedProcess* sandbox) const;
+
+ private:
+  const std::string archive_type_;
+  const std::string extension_;
+  Metrics* const metrics_;
+  const std::string metrics_name_;
+  const std::vector<int> password_needed_exit_codes_;
+  const std::unique_ptr<SandboxedProcessFactory> sandbox_factory_;
+
+  friend class ArchiveMounterTest;
+};
+
+}  // namespace cros_disks
+
+#endif  // CROS_DISKS_ARCHIVE_MOUNTER_H_
diff --git a/cros-disks/archive_mounter_test.cc b/cros-disks/archive_mounter_test.cc
new file mode 100644
index 0000000..c9e1cfc
--- /dev/null
+++ b/cros-disks/archive_mounter_test.cc
@@ -0,0 +1,193 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "cros-disks/archive_mounter.h"
+
+#include <utility>
+
+#include <base/files/file_util.h>
+#include <base/files/scoped_temp_dir.h>
+#include <base/strings/string_util.h>
+#include <base/strings/string_split.h>
+#include <brillo/process/process_reaper.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "cros-disks/mount_point.h"
+#include "cros-disks/platform.h"
+
+namespace cros_disks {
+
+namespace {
+
+using testing::_;
+using testing::ElementsAre;
+using testing::Return;
+using testing::UnorderedElementsAre;
+
+const char kArchiveType[] = "archive";
+const char kSomeSource[] = "/home/user/something.archive";
+const char kMountDir[] = "/mnt";
+const int kPasswordNeededCode = 42;
+
+// Mock Platform implementation for testing.
+class MockFUSEPlatform : public Platform {
+ public:
+  MOCK_METHOD(bool, PathExists, (const std::string&), (const, override));
+};
+
+class FakeSandboxedProcessFactory : public SandboxedProcessFactory {
+ public:
+  std::unique_ptr<SandboxedProcess> CreateSandboxedProcess() const override {
+    return std::make_unique<FakeSandboxedProcess>();
+  }
+};
+
+}  // namespace
+
+class ArchiveMounterTest : public ::testing::Test {
+ public:
+  ArchiveMounterTest() {
+    ON_CALL(platform_, PathExists).WillByDefault(Return(true));
+  }
+
+ protected:
+  std::unique_ptr<ArchiveMounter> CreateMounter(
+      std::vector<int> password_needed_codes) {
+    return std::make_unique<ArchiveMounter>(
+        &platform_, &process_reaper_, kArchiveType, &metrics_, "ArchiveMetrics",
+        std::move(password_needed_codes),
+        std::make_unique<FakeSandboxedProcessFactory>());
+  }
+
+  MountErrorType InterpretReturnCode(const ArchiveMounter& mounter,
+                                     int exit_code) const {
+    return mounter.InterpretReturnCode(exit_code);
+  }
+
+  std::unique_ptr<FakeSandboxedProcess> PrepareSandbox(
+      const ArchiveMounter& mounter,
+      const std::string& source,
+      std::vector<std::string> params,
+      MountErrorType* error) const {
+    auto sandbox = mounter.PrepareSandbox(source, base::FilePath(kMountDir),
+                                          std::move(params), error);
+    return std::unique_ptr<FakeSandboxedProcess>(
+        static_cast<FakeSandboxedProcess*>(sandbox.release()));
+  }
+
+  MockFUSEPlatform platform_;
+  brillo::ProcessReaper process_reaper_;
+  Metrics metrics_;
+};
+
+TEST_F(ArchiveMounterTest, CanMount) {
+  auto mounter = CreateMounter({});
+  base::FilePath name;
+  EXPECT_TRUE(mounter->CanMount("/foo/bar/baz.archive", {}, &name));
+  EXPECT_EQ("baz.archive", name.value());
+  EXPECT_FALSE(mounter->CanMount("/foo/bar/baz.something", {}, &name));
+}
+
+TEST_F(ArchiveMounterTest, InvalidPathsRejected) {
+  auto mounter = CreateMounter({});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox = PrepareSandbox(*mounter, "foo.archive", {}, &error);
+  EXPECT_NE(MOUNT_ERROR_NONE, error);
+  EXPECT_FALSE(sandbox);
+  sandbox = PrepareSandbox(*mounter, "/foo/../etc/foo.archive", {}, &error);
+  EXPECT_NE(MOUNT_ERROR_NONE, error);
+  EXPECT_FALSE(sandbox);
+}
+
+TEST_F(ArchiveMounterTest, AppArgs) {
+  auto mounter = CreateMounter({});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox = PrepareSandbox(*mounter, kSomeSource, {}, &error);
+  EXPECT_EQ(MOUNT_ERROR_NONE, error);
+  ASSERT_TRUE(sandbox);
+  EXPECT_THAT(sandbox->arguments(), ElementsAre("-o", _, kSomeSource));
+  std::vector<std::string> opts =
+      base::SplitString(sandbox->arguments()[1], ",", base::KEEP_WHITESPACE,
+                        base::SPLIT_WANT_ALL);
+  EXPECT_THAT(opts,
+              UnorderedElementsAre("umask=0222", "uid=1000", "gid=1001", "ro"));
+}
+
+TEST_F(ArchiveMounterTest, FileNotFound) {
+  EXPECT_CALL(platform_, PathExists(kSomeSource)).WillRepeatedly(Return(false));
+  auto mounter = CreateMounter({});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox = PrepareSandbox(*mounter, kSomeSource, {}, &error);
+  EXPECT_NE(MOUNT_ERROR_NONE, error);
+  EXPECT_FALSE(sandbox);
+}
+
+TEST_F(ArchiveMounterTest, AppNeedsPassword) {
+  auto mounter = CreateMounter({kPasswordNeededCode});
+  EXPECT_EQ(MOUNT_ERROR_NEED_PASSWORD,
+            InterpretReturnCode(*mounter, kPasswordNeededCode));
+}
+
+TEST_F(ArchiveMounterTest, WithPassword) {
+  const std::string password = "My Password";
+
+  auto mounter = CreateMounter({kPasswordNeededCode});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox =
+      PrepareSandbox(*mounter, kSomeSource, {"password=" + password}, &error);
+  EXPECT_EQ(MOUNT_ERROR_NONE, error);
+  ASSERT_TRUE(sandbox);
+  EXPECT_EQ(password, sandbox->input());
+  // Make sure password is not in args.
+  std::vector<std::string> opts =
+      base::SplitString(sandbox->arguments()[1], ",", base::KEEP_WHITESPACE,
+                        base::SPLIT_WANT_ALL);
+  EXPECT_THAT(opts,
+              UnorderedElementsAre("umask=0222", "uid=1000", "gid=1001", "ro"));
+}
+
+TEST_F(ArchiveMounterTest, NoPassword) {
+  auto mounter = CreateMounter({kPasswordNeededCode});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox = PrepareSandbox(*mounter, kSomeSource,
+                                {
+                                    "Password=1",  // Options are case sensitive
+                                    "password =2",  // Space is significant
+                                    " password=3",  // Space is significant
+                                    "password",     // Not a valid option
+                                },
+                                &error);
+  ASSERT_TRUE(sandbox);
+  EXPECT_EQ("", sandbox->input());
+}
+
+TEST_F(ArchiveMounterTest, CopiesPassword) {
+  for (const std::string password : {
+           "",
+           " ",
+           "=",
+           "simple",
+           R"( !@#$%^&*()_-+={[}]|\:;"'<,>.?/ )",
+       }) {
+    auto mounter = CreateMounter({kPasswordNeededCode});
+    MountErrorType error = MOUNT_ERROR_UNKNOWN;
+    auto sandbox =
+        PrepareSandbox(*mounter, kSomeSource, {"password=" + password}, &error);
+    ASSERT_TRUE(sandbox);
+    EXPECT_EQ(password, sandbox->input());
+  }
+}
+
+TEST_F(ArchiveMounterTest, IgnoredIfNotNeeded) {
+  auto mounter = CreateMounter({});
+  MountErrorType error = MOUNT_ERROR_UNKNOWN;
+  auto sandbox =
+      PrepareSandbox(*mounter, kSomeSource, {"password=foo"}, &error);
+  EXPECT_EQ(MOUNT_ERROR_NONE, error);
+  ASSERT_TRUE(sandbox);
+  EXPECT_EQ("", sandbox->input());
+}
+
+}  // namespace cros_disks
diff --git a/cros-disks/disk_manager.cc b/cros-disks/disk_manager.cc
index d7cbb9f..cceefc6 100644
--- a/cros-disks/disk_manager.cc
+++ b/cros-disks/disk_manager.cc
@@ -47,10 +47,7 @@
                   SandboxedExecutable executable,
                   OwnerUser run_as,
                   std::vector<std::string> options)
-      : FUSEMounter(platform,
-                    reaper,
-                    std::move(filesystem_type),
-                    /* nosymfollow= */ true),
+      : FUSEMounter(platform, reaper, std::move(filesystem_type), {}),
         upstream_factory_(upstream_factory),
         sandbox_factory_(platform,
                          std::move(executable),
diff --git a/cros-disks/fuse_mounter.cc b/cros-disks/fuse_mounter.cc
index 5b9995a..5ac6888 100644
--- a/cros-disks/fuse_mounter.cc
+++ b/cros-disks/fuse_mounter.cc
@@ -42,25 +42,8 @@
 
 namespace {
 
-const mode_t kSourcePathPermissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
-
 const char kFuseDeviceFile[] = "/dev/fuse";
 
-template <typename T>
-base::Optional<T> UnsetIfEmpty(T value) {
-  if (value.empty())
-    return base::Optional<T>();
-  return base::Optional<T>(std::move(value));
-}
-
-// TODO(dats): Remove when it's done beforehead by the caller.
-OwnerUser ResolveUserOrDie(const Platform* platform, const std::string& user) {
-  OwnerUser result;
-  PCHECK(platform->GetUserAndGroupId(user, &result.uid, &result.gid));
-  result.gid = kChronosAccessGID;
-  return result;
-}
-
 class FUSEMountPoint : public MountPoint {
  public:
   FUSEMountPoint(const base::FilePath& path, const Platform* platform)
@@ -286,11 +269,11 @@
 FUSEMounter::FUSEMounter(const Platform* platform,
                          brillo::ProcessReaper* process_reaper,
                          std::string filesystem_type,
-                         bool nosymfollow)
+                         Config config)
     : platform_(platform),
       process_reaper_(process_reaper),
       filesystem_type_(std::move(filesystem_type)),
-      nosymfollow_(nosymfollow) {}
+      config_(std::move(config)) {}
 
 FUSEMounter::~FUSEMounter() = default;
 
@@ -300,7 +283,7 @@
     std::vector<std::string> params,
     MountErrorType* error) const {
   // Read-only is the only parameter that has any effect at this layer.
-  const bool read_only = IsReadOnlyMount(params);
+  const bool read_only = config_.read_only || IsReadOnlyMount(params);
 
   const base::File fuse_file = base::File(
       base::FilePath(kFuseDeviceFile),
@@ -358,7 +341,7 @@
   *error = platform_->Mount(source_descr, target_path.value(), fuse_type,
                             MountOptions::kMountFlags | MS_DIRSYNC |
                                 (read_only ? MS_RDONLY : 0) |
-                                (nosymfollow_ ? MS_NOSYMFOLLOW : 0),
+                                (config_.nosymfollow ? MS_NOSYMFOLLOW : 0),
                             fuse_mount_options);
 
   if (*error != MOUNT_ERROR_NONE) {
@@ -443,8 +426,10 @@
     std::string filesystem_type,
     bool nosymfollow,
     const SandboxedProcessFactory* sandbox_factory)
-    : FUSEMounter(
-          platform, process_reaper, std::move(filesystem_type), nosymfollow),
+    : FUSEMounter(platform,
+                  process_reaper,
+                  std::move(filesystem_type),
+                  {.nosymfollow = nosymfollow}),
       sandbox_factory_(sandbox_factory) {}
 
 FUSEMounterHelper::~FUSEMounterHelper() = default;
@@ -463,134 +448,4 @@
   return sandbox;
 }
 
-FUSEMounterLegacy::FUSEMounterLegacy(Params params)
-    : FUSEMounter(params.platform,
-                  params.process_reaper,
-                  std::move(params.filesystem_type),
-                  params.nosymfollow),
-      metrics_(params.metrics),
-      metrics_name_(std::move(params.metrics_name)),
-      seccomp_policy_(),
-      bind_paths_(std::move(params.bind_paths)),
-      password_needed_codes_(std::move(params.password_needed_codes)),
-      mount_options_(std::move(params.mount_options)),
-      sandbox_factory_(
-          platform(),
-          {base::FilePath(std::move(params.mount_program)),
-           UnsetIfEmpty(base::FilePath(std::move(params.seccomp_policy)))},
-          ResolveUserOrDie(platform(), params.mount_user),
-          params.network_access,
-          std::move(params.supplementary_groups),
-          UnsetIfEmpty(base::FilePath(std::move(params.mount_namespace)))) {}
-
-void FUSEMounterLegacy::CopyPassword(const std::vector<std::string>& options,
-                                     Process* const process) const {
-  DCHECK(process);
-
-  // Is the process "password-aware"?
-  if (password_needed_codes_.empty())
-    return;
-
-  // Is there a password available in options?
-  const base::StringPiece prefix = "password=";
-  const auto it = std::find_if(options.cbegin(), options.cend(),
-                               [prefix](const base::StringPiece s) {
-                                 return base::StartsWith(s, prefix);
-                               });
-  if (it == options.cend())
-    return;
-
-  // Pass the password via stdin.
-  process->SetStdIn(it->substr(prefix.size()));
-}
-
-std::unique_ptr<SandboxedProcess> FUSEMounterLegacy::PrepareSandbox(
-    const std::string& source,
-    const base::FilePath& target_path,
-    std::vector<std::string> params,
-    MountErrorType* error) const {
-  std::unique_ptr<SandboxedProcess> mount_process = CreateSandboxedProcess();
-
-  // TODO(crbug.com/1053778) Only create the necessary tmpfs filesystems.
-  for (const char* const dir : {"/home", "/media"}) {
-    if (!mount_process->Mount("tmpfs", dir, "tmpfs", "mode=0755,size=10M")) {
-      LOG(ERROR) << "Cannot mount " << quote(dir);
-      *error = MOUNT_ERROR_INTERNAL;
-      return nullptr;
-    }
-  }
-
-  // Data dirs if any are mounted inside /run/fuse.
-  if (!mount_process->BindMount("/run/fuse", "/run/fuse", false, false)) {
-    LOG(ERROR) << "Can't bind /run/fuse";
-    return nullptr;
-  }
-
-  // If a block device is being mounted, bind mount it into the sandbox.
-  if (base::StartsWith(source, "/dev/", base::CompareCase::SENSITIVE)) {
-    // Re-own source.
-    if (!platform()->SetOwnership(source, getuid(),
-                                  sandbox_factory_.run_as().gid) ||
-        !platform()->SetPermissions(source, kSourcePathPermissions)) {
-      LOG(ERROR) << "Can't set up permissions on " << quote(source);
-      *error = MOUNT_ERROR_INSUFFICIENT_PERMISSIONS;
-      return nullptr;
-    }
-
-    if (!mount_process->BindMount(source, source, true, false)) {
-      LOG(ERROR) << "Cannot bind mount device " << quote(source);
-      *error = MOUNT_ERROR_INVALID_ARGUMENT;
-      return nullptr;
-    }
-  }
-
-  // This is for additional data dirs.
-  for (const BindPath& bind_path : bind_paths_) {
-    if (!mount_process->BindMount(bind_path.path, bind_path.path,
-                                  bind_path.writable, bind_path.recursive)) {
-      LOG(ERROR) << "Cannot bind-mount " << quote(bind_path.path);
-      *error = MOUNT_ERROR_INVALID_ARGUMENT;
-      return nullptr;
-    }
-  }
-
-  {
-    // TODO(dats): This it leaking legacy options implementation. Remove it.
-    std::string options_string = mount_options_.ToFuseMounterOptions();
-    DCHECK(!options_string.empty());
-    mount_process->AddArgument("-o");
-    mount_process->AddArgument(std::move(options_string));
-  }
-
-  if (!source.empty()) {
-    mount_process->AddArgument(source);
-  }
-
-  CopyPassword(params, mount_process.get());
-
-  return mount_process;
-}
-
-MountErrorType FUSEMounterLegacy::InterpretReturnCode(int return_code) const {
-  if (metrics_ && !metrics_name_.empty())
-    metrics_->RecordFuseMounterErrorCode(metrics_name_, return_code);
-
-  if (base::Contains(password_needed_codes_, return_code))
-    return MOUNT_ERROR_NEED_PASSWORD;
-
-  return FUSEMounter::InterpretReturnCode(return_code);
-}
-
-bool FUSEMounterLegacy::CanMount(const std::string& source,
-                                 const std::vector<std::string>& params,
-                                 base::FilePath* suggested_name) const {
-  NOTREACHED();
-  return true;
-}
-
-std::unique_ptr<SandboxedProcess> FUSEMounterLegacy::CreateSandboxedProcess()
-    const {
-  return sandbox_factory_.CreateSandboxedProcess();
-}
-
 }  // namespace cros_disks
diff --git a/cros-disks/fuse_mounter.h b/cros-disks/fuse_mounter.h
index 7da29bd..0001883 100644
--- a/cros-disks/fuse_mounter.h
+++ b/cros-disks/fuse_mounter.h
@@ -80,10 +80,15 @@
 // and sandboxing is to be done in a subclass.
 class FUSEMounter : public Mounter {
  public:
+  struct Config {
+    bool nosymfollow = true;
+    bool read_only = false;
+  };
+
   FUSEMounter(const Platform* platform,
               brillo::ProcessReaper* process_reaper,
               std::string filesystem_type,
-              bool nosymfollow);
+              Config config);
   FUSEMounter(const FUSEMounter&) = delete;
   FUSEMounter& operator=(const FUSEMounter&) = delete;
   ~FUSEMounter() override;
@@ -129,7 +134,7 @@
   const Platform* const platform_;
   brillo::ProcessReaper* const process_reaper_;
   const std::string filesystem_type_;
-  const bool nosymfollow_;
+  const Config config_;
 };
 
 // A convenience class to tie FUSE mounter with a sandbox configuration.
@@ -165,123 +170,6 @@
   const SandboxedProcessFactory* const sandbox_factory_;
 };
 
-// A class for mounting something using a FUSE mount program.
-// TODO(dats): It contains too much logic used only in some cases but
-// not others. Tear it apart.
-class FUSEMounterLegacy : public FUSEMounter {
- public:
-  struct BindPath {
-    std::string path;
-    bool writable = false;
-    bool recursive = false;
-  };
-
-  using BindPaths = std::vector<BindPath>;
-
-  // Parameters passed to FUSEMounter's constructor.
-  // Members are kept in alphabetical order.
-  struct Params {
-    // Paths the FUSE mount program needs to access (beyond basic /proc, /dev,
-    // etc).
-    BindPaths bind_paths;
-
-    // Filesystem type.
-    std::string filesystem_type;
-
-    // Optional object that collects UMA metrics.
-    Metrics* metrics = nullptr;
-
-    // Name of the UMA histogram recording the FUSE mount program return code.
-    // Not recorded if empty or if metrics is null.
-    std::string metrics_name;
-
-    // Optional mount namespace where the source path exists.
-    std::string mount_namespace;
-
-    // FUSE mount options.
-    MountOptions mount_options;
-
-    // Path of the FUSE mount program.
-    std::string mount_program;
-
-    // User to run the FUSE mount program as.
-    std::string mount_user;
-
-    // Whether the FUSE mount program needs to access the network.
-    bool network_access = false;
-
-    // By default it's mounted with symlinks following disabled.
-    bool nosymfollow = true;
-
-    // Possible codes returned by the FUSE mount program to ask for a password.
-    std::vector<int> password_needed_codes;
-
-    // Object that provides platform service.
-    const Platform* platform = nullptr;
-
-    // Process reaper to monitor FUSE daemons.
-    brillo::ProcessReaper* process_reaper = nullptr;
-
-    // Optional path to BPF seccomp filter policy.
-    std::string seccomp_policy;
-
-    // Supplementary groups to run the mount program with.
-    std::vector<gid_t> supplementary_groups;
-  };
-
-  explicit FUSEMounterLegacy(Params params);
-  FUSEMounterLegacy(const FUSEMounterLegacy&) = delete;
-  FUSEMounterLegacy& operator=(const FUSEMounterLegacy&) = delete;
-
-  // If necessary, extracts the password from the given options and sets the
-  // standard input of the given process. Does nothing if password_needed_codes
-  // is empty. Does nothing if no string starting with "password=" is found in
-  // options. If several options start with "password=", only the first one is
-  // taken in account and the other ones are ignored.
-  void CopyPassword(const std::vector<std::string>& options,
-                    Process* process) const;
-
-  const MountOptions& mount_options() const { return mount_options_; }
-
- protected:
-  // FUSEMounter overrides:
-  bool CanMount(const std::string& source,
-                const std::vector<std::string>& params,
-                base::FilePath* suggested_name) const override;
-
-  MountErrorType InterpretReturnCode(int return_code) const override;
-
-  std::unique_ptr<SandboxedProcess> PrepareSandbox(
-      const std::string& source,
-      const base::FilePath& target_path,
-      std::vector<std::string> params,
-      MountErrorType* error) const override;
-
-  // Protected for mocking out in testing.
-  virtual std::unique_ptr<SandboxedProcess> CreateSandboxedProcess() const;
-
-  // An object that collects UMA metrics.
-  Metrics* const metrics_;
-
-  // Name of the UMA histogram recording the FUSE mount program return code.
-  // Not recorded if empty or if metrics is null.
-  const std::string metrics_name_;
-
-  // If not empty the path to BPF seccomp filter policy.
-  const std::string seccomp_policy_;
-
-  // Paths the FUSE mount program needs to access (beyond basic /proc, /dev,
-  // etc).
-  const BindPaths bind_paths_;
-
-  // Possible codes returned by the FUSE mount program to ask for a password.
-  std::vector<int> password_needed_codes_;
-
-  const MountOptions mount_options_;
-
-  const FUSESandboxedProcessFactory sandbox_factory_;
-};
-
 }  // namespace cros_disks
 
 #endif  // CROS_DISKS_FUSE_MOUNTER_H_
diff --git a/cros-disks/fuse_mounter_test.cc b/cros-disks/fuse_mounter_test.cc
index e4fc5aa..10f03e6 100644
--- a/cros-disks/fuse_mounter_test.cc
+++ b/cros-disks/fuse_mounter_test.cc
@@ -44,10 +44,8 @@
 const gid_t kMountGID = 201;
 const char kMountUser[] = "fuse-fuse";
 const char kFUSEType[] = "fusefs";
-const char kMountProgram[] = "/bin/dummy";
 const char kSomeSource[] = "/dev/dummy";
 const char kMountDir[] = "/mnt";
-const int kPasswordNeededCode = 42;
 
 // Mock Platform implementation for testing.
 class MockFUSEPlatform : public Platform {
@@ -133,8 +131,7 @@
  public:
   FUSEMounterForTesting(const Platform* platform,
                         brillo::ProcessReaper* process_reaper)
-      : FUSEMounter(
-            platform, process_reaper, kFUSEType, true /* nosymfollow */) {}
+      : FUSEMounter(platform, process_reaper, kFUSEType, {}) {}
 
   MOCK_METHOD(std::unique_ptr<SandboxedProcess>,
               PrepareSandbox,
@@ -428,167 +425,4 @@
   EXPECT_EQ(MOUNT_ERROR_NONE, mount_point->Unmount());
 }
 
-namespace {
-
-class FUSEMounterLegacyForTesting : public FUSEMounterLegacy {
- public:
-  FUSEMounterLegacyForTesting(const Platform* platform,
-                              brillo::ProcessReaper* process_reaper)
-      : FUSEMounterLegacy({.filesystem_type = kFUSEType,
-                           .mount_program = kMountProgram,
-                           .mount_user = kMountUser,
-                           .password_needed_codes = {kPasswordNeededCode},
-                           .platform = platform,
-                           .process_reaper = process_reaper}) {}
-
-  MOCK_METHOD(int, OnInput, (const std::string&), (const));
-  MOCK_METHOD(int, InvokeMountTool, (const std::vector<std::string>&), (const));
-
-  mutable std::vector<std::string> environment;
-
- private:
-  std::unique_ptr<SandboxedProcess> CreateSandboxedProcess() const override {
-    auto mock = std::make_unique<MockSandboxedProcess>();
-    const SandboxedProcess* const process = mock.get();
-    mock->AddArgument(kMountProgram);
-    ON_CALL(*mock, StartImpl(_, _, _)).WillByDefault(Return(123));
-    ON_CALL(*mock, WaitNonBlockingImpl())
-        .WillByDefault(Invoke([this, process]() {
-          const std::string& input = process->input();
-          if (!input.empty())
-            OnInput(input);
-
-          return InvokeMountTool(process->arguments());
-        }));
-    return mock;
-  }
-};
-
-}  // namespace
-
-class FUSEMounterLegacyTest : public ::testing::Test {
- public:
-  FUSEMounterLegacyTest() : mounter_(&platform_, &process_reaper_) {
-    ON_CALL(platform_, Mount(kSomeSource, kMountDir, _, _, _))
-        .WillByDefault(Return(MOUNT_ERROR_NONE));
-  }
-
- protected:
-  // Sets up mock expectations for a successful mount.
-  void SetupMountExpectations() {
-    EXPECT_CALL(mounter_, InvokeMountTool(ElementsAre(
-                              kMountProgram, "-o", MountOptions().ToString(),
-                              kSomeSource, StartsWith("/dev/fd/"))))
-        .WillOnce(Return(0));
-    EXPECT_CALL(platform_,
-                SetOwnership(kSomeSource, getuid(), kChronosAccessGID))
-        .WillOnce(Return(true));
-    EXPECT_CALL(platform_, SetPermissions(kSomeSource, S_IRUSR | S_IWUSR |
-                                                           S_IRGRP | S_IWGRP))
-        .WillOnce(Return(true));
-    EXPECT_CALL(platform_, SetOwnership(kMountDir, _, _)).Times(0);
-    EXPECT_CALL(platform_, SetPermissions(kMountDir, _)).Times(0);
-  }
-
-  MockFUSEPlatform platform_;
-  brillo::ProcessReaper process_reaper_;
-  FUSEMounterLegacyForTesting mounter_;
-};
-
-TEST_F(FUSEMounterLegacyTest, AppNeedsPassword) {
-  EXPECT_CALL(platform_, Unmount(_, _)).WillOnce(Return(MOUNT_ERROR_NONE));
-  EXPECT_CALL(mounter_, InvokeMountTool(_))
-      .WillOnce(Return(kPasswordNeededCode));
-
-  MountErrorType error = MOUNT_ERROR_UNKNOWN;
-  auto mount_point =
-      mounter_.Mount(kSomeSource, base::FilePath(kMountDir), {}, &error);
-  EXPECT_FALSE(mount_point);
-  EXPECT_EQ(MOUNT_ERROR_NEED_PASSWORD, error);
-}
-
-TEST_F(FUSEMounterLegacyTest, WithPassword) {
-  const std::string password = "My Password";
-
-  SetupMountExpectations();
-  EXPECT_CALL(mounter_, OnInput(password)).Times(1);
-  // The MountPoint returned by Mount() will unmount when it is destructed.
-  EXPECT_CALL(platform_, Unmount(kMountDir, 0))
-      .WillOnce(Return(MOUNT_ERROR_NONE));
-
-  MountErrorType error = MOUNT_ERROR_UNKNOWN;
-  auto mount_point = mounter_.Mount(kSomeSource, base::FilePath(kMountDir),
-                                    {"password=" + password}, &error);
-  EXPECT_TRUE(mount_point);
-  EXPECT_EQ(MOUNT_ERROR_NONE, error);
-}
-
-TEST(FUSEMounterPasswordTest, NoPassword) {
-  MockFUSEPlatform platform;
-  const FUSEMounterLegacy mounter({
-      .mount_program = kMountProgram,
-      .mount_user = kMountUser,
-      .password_needed_codes = {kPasswordNeededCode},
-      .platform = &platform,
-  });
-  SandboxedProcess process;
-  mounter.CopyPassword(
-      {
-          "Password=1",   // Options are case sensitive
-          "password =2",  // Space is significant
-          " password=3",  // Space is significant
-          "password",     // Not a valid option
-      },
-      &process);
-  EXPECT_EQ(process.input(), "");
-}
-
-TEST(FUSEMounterPasswordTest, CopiesPassword) {
-  MockFUSEPlatform platform;
-  const FUSEMounterLegacy mounter({
-      .mount_program = kMountProgram,
-      .mount_user = kMountUser,
-      .password_needed_codes = {kPasswordNeededCode},
-      .platform = &platform,
-  });
-  for (const std::string password : {
-           "",
-           " ",
-           "=",
-           "simple",
-           R"( !@#$%^&*()_-+={[}]|\:;"'<,>.?/ )",
-       }) {
-    SandboxedProcess process;
-    mounter.CopyPassword({"password=" + password}, &process);
-    EXPECT_EQ(process.input(), password);
-  }
-}
-
-TEST(FUSEMounterPasswordTest, FirstPassword) {
-  MockFUSEPlatform platform;
-  const FUSEMounterLegacy mounter({
-      .mount_program = kMountProgram,
-      .mount_user = kMountUser,
-      .password_needed_codes = {kPasswordNeededCode},
-      .platform = &platform,
-  });
-  SandboxedProcess process;
-  mounter.CopyPassword({"other1=value1", "password=1", "password=2",
-                        "other2=value2", "password=3"},
-                       &process);
-  EXPECT_EQ(process.input(), "1");
-}
-
-TEST(FUSEMounterPasswordTest, IgnoredIfNotNeeded) {
-  MockFUSEPlatform platform;
-  const FUSEMounterLegacy mounter({
-      .mount_program = kMountProgram,
-      .mount_user = kMountUser,
-      .platform = &platform,
-  });
-  SandboxedProcess process;
-  mounter.CopyPassword({"password=dummy"}, &process);
-  EXPECT_EQ(process.input(), "");
-}
-
 }  // namespace cros_disks
diff --git a/cros-disks/rar_manager.cc b/cros-disks/rar_manager.cc
index b908bbe..cf89f75 100644
--- a/cros-disks/rar_manager.cc
+++ b/cros-disks/rar_manager.cc
@@ -12,7 +12,9 @@
 #include <base/files/file_util.h>
 #include <base/logging.h>
 #include <base/strings/string_util.h>
+#include <base/strings/stringprintf.h>
 
+#include "cros-disks/archive_mounter.h"
 #include "cros-disks/error_logger.h"
 #include "cros-disks/fuse_mounter.h"
 #include "cros-disks/metrics.h"
@@ -24,8 +26,77 @@
 
 const char kExtension[] = ".rar";
 
+OwnerUser GetRarUserOrDie(const Platform* platform) {
+  OwnerUser run_as;
+  PCHECK(platform->GetUserAndGroupId("fuse-rar2fs", &run_as.uid, &run_as.gid))
+      << "Cannot resolve required user fuse-rar2fs";
+  return run_as;
+}
+
 }  // namespace
 
+class RarManager::RarMounter : public ArchiveMounter {
+ public:
+  RarMounter(const Platform* platform,
+             brillo::ProcessReaper* process_reaper,
+             Metrics* metrics,
+             const RarManager* rar_manager)
+      : ArchiveMounter(
+            platform,
+            process_reaper,
+            "rar",
+            metrics,
+            "Rar2fs",
+            {12,   // ERAR_BAD_DATA
+             22,   // ERAR_MISSING_PASSWORD
+             24},  // ERAR_BAD_PASSWORD
+            std::make_unique<FUSESandboxedProcessFactory>(
+                platform,
+                SandboxedExecutable{
+                    base::FilePath("/usr/bin/rar2fs"),
+                    base::FilePath("/usr/share/policy/rar2fs-seccomp.policy")},
+                GetRarUserOrDie(platform),
+                /* has_network_access= */ false,
+                rar_manager->GetSupplementaryGroups())),
+        rar_manager_(rar_manager) {}
+
+ protected:
+  MountErrorType FormatInvocationCommand(
+      const base::FilePath& archive,
+      std::vector<std::string> params,
+      SandboxedProcess* sandbox) const override {
+    // Bind-mount parts of a multipart archive if any.
+    for (const auto& path : rar_manager_->GetBindPaths(archive.value())) {
+      if (!sandbox->BindMount(path, path, /* writeable= */ false,
+                              /* recursive= */ false)) {
+        PLOG(ERROR) << "Could not bind " << quote(path);
+        return MOUNT_ERROR_INTERNAL;
+      }
+    }
+
+    std::vector<std::string> opts = {
+        MountOptions::kOptionReadOnly, "umask=0222", "locale=en_US.UTF8",
+        base::StringPrintf("uid=%d", kChronosUID),
+        base::StringPrintf("gid=%d", kChronosAccessGID)};
+
+    sandbox->AddArgument("-o");
+    sandbox->AddArgument(base::JoinString(opts, ","));
+    sandbox->AddArgument(archive.value());
+
+    return MOUNT_ERROR_NONE;
+  }
+
+  const RarManager* rar_manager_;
+};
+
+RarManager::RarManager(const std::string& mount_root,
+                       Platform* platform,
+                       Metrics* metrics,
+                       brillo::ProcessReaper* process_reaper)
+    : ArchiveManager(mount_root, platform, metrics, process_reaper),
+      mounter_(std::make_unique<RarMounter>(
+          platform, process_reaper, metrics, this)) {}
+
 RarManager::~RarManager() {
   UnmountAll();
 }
@@ -44,11 +115,7 @@
     const base::FilePath& mount_path,
     MountOptions* const applied_options,
     MountErrorType* const error) {
-  DCHECK(applied_options);
   DCHECK(error);
-
-  metrics()->RecordArchiveType("rar");
-
   // MountManager resolves source path to real path before calling DoMount,
   // so no symlinks or '..' will be here.
   if (!IsInAllowedFolder(source_path)) {
@@ -56,39 +123,7 @@
     *error = MOUNT_ERROR_INVALID_DEVICE_PATH;
     return nullptr;
   }
-
-  MountNamespace mount_namespace = GetMountNamespaceFor(source_path);
-
-  FUSEMounterLegacy::Params params{
-      .bind_paths = GetBindPaths(source_path),
-      .filesystem_type = "rarfs",
-      .metrics = metrics(),
-      .metrics_name = "Rar2fs",
-      .mount_namespace = std::move(mount_namespace.name),
-      .mount_program = "/usr/bin/rar2fs",
-      .mount_user = "fuse-rar2fs",
-      .password_needed_codes = {12,   // ERAR_BAD_DATA
-                                22,   // ERAR_MISSING_PASSWORD
-                                24},  // ERAR_BAD_PASSWORD
-      .platform = platform(),
-      .process_reaper = process_reaper(),
-      .seccomp_policy = "/usr/share/policy/rar2fs-seccomp.policy",
-      .supplementary_groups = GetSupplementaryGroups(),
-  };
-
-  mount_namespace.guard.reset();
-
-  // Prepare FUSE mount options.
-  params.mount_options.EnforceOption("locale=en_US.UTF8");
-  *error = GetMountOptions(&params.mount_options);
-  if (*error != MOUNT_ERROR_NONE)
-    return nullptr;
-
-  *applied_options = params.mount_options;
-
-  // Run rar2fs.
-  const FUSEMounterLegacy mounter(std::move(params));
-  return mounter.Mount(source_path, mount_path, options, error);
+  return mounter_->Mount(source_path, mount_path, options, error);
 }
 
 bool RarManager::Increment(const std::string::iterator begin,
@@ -134,7 +169,7 @@
 }
 
 void RarManager::AddPathsWithOldNamingScheme(
-    FUSEMounterLegacy::BindPaths* const bind_paths,
+    std::vector<std::string>* const bind_paths,
     const base::StringPiece original_path) const {
   DCHECK(bind_paths);
 
@@ -155,18 +190,18 @@
   if (!platform()->PathExists(candidate_path))
     return;
 
-  bind_paths->push_back({candidate_path});
+  bind_paths->push_back(candidate_path);
 
   // Iterate by incrementing the last 3 characters of the extension:
   // '.r00' -> '.r01' -> ... -> '.r99' -> '.s00' -> ... -> '.z99'
   // or
   // '.R00' -> '.R01' -> ... -> '.R99' -> '.S00' -> ... -> '.Z99'
   while (Increment(end - 3, end) && platform()->PathExists(candidate_path))
-    bind_paths->push_back({candidate_path});
+    bind_paths->push_back(candidate_path);
 }
 
 void RarManager::AddPathsWithNewNamingScheme(
-    FUSEMounterLegacy::BindPaths* const bind_paths,
+    std::vector<std::string>* const bind_paths,
     const base::StringPiece original_path,
     const IndexRange& digits) const {
   DCHECK(bind_paths);
@@ -186,13 +221,13 @@
   // Find all the files making the multipart archive.
   while (Increment(begin, end) && platform()->PathExists(candidate_path)) {
     if (candidate_path != original_path)
-      bind_paths->push_back({candidate_path});
+      bind_paths->push_back(candidate_path);
   }
 }
 
-FUSEMounterLegacy::BindPaths RarManager::GetBindPaths(
+std::vector<std::string> RarManager::GetBindPaths(
     const base::StringPiece original_path) const {
-  FUSEMounterLegacy::BindPaths bind_paths = {{std::string(original_path)}};
+  std::vector<std::string> bind_paths = {std::string(original_path)};
 
   // Delimit the digit range assuming original_path uses the new naming scheme.
   const IndexRange digits = ParseDigits(original_path);
diff --git a/cros-disks/rar_manager.h b/cros-disks/rar_manager.h
index 3c4cdb4..4d2cb66 100644
--- a/cros-disks/rar_manager.h
+++ b/cros-disks/rar_manager.h
@@ -17,10 +17,17 @@
 
 namespace cros_disks {
 
+class ArchiveMounter;
+
 // A MountManager mounting RAR archives as virtual filesystems using rar2fs.
 class RarManager : public ArchiveManager {
  public:
-  using ArchiveManager::ArchiveManager;
+  RarManager(const std::string& mount_root,
+             Platform* platform,
+             Metrics* metrics,
+             brillo::ProcessReaper* process_reaper);
+  RarManager(const RarManager&) = delete;
+  RarManager& operator=(const RarManager&) = delete;
 
   ~RarManager() override;
 
@@ -63,11 +70,11 @@
   static IndexRange ParseDigits(base::StringPiece path);
 
   // Adds bind paths using old naming scheme.
-  void AddPathsWithOldNamingScheme(FUSEMounterLegacy::BindPaths* bind_paths,
+  void AddPathsWithOldNamingScheme(std::vector<std::string>* bind_paths,
                                    base::StringPiece original_path) const;
 
   // Adds bind paths using new naming scheme.
-  void AddPathsWithNewNamingScheme(FUSEMounterLegacy::BindPaths* bind_paths,
+  void AddPathsWithNewNamingScheme(std::vector<std::string>* bind_paths,
                                    base::StringPiece original_path,
                                    const IndexRange& digits) const;
 
@@ -111,8 +118,10 @@
   // ...
   // basename999.rar
   // etc.
-  FUSEMounterLegacy::BindPaths GetBindPaths(
-      base::StringPiece original_path) const;
+  std::vector<std::string> GetBindPaths(base::StringPiece original_path) const;
+
+  class RarMounter;
+  const std::unique_ptr<ArchiveMounter> mounter_;
 
   FRIEND_TEST(RarManagerTest, CanMount);
   FRIEND_TEST(RarManagerTest, SuggestMountPath);
diff --git a/cros-disks/rar_manager_test.cc b/cros-disks/rar_manager_test.cc
index b017952..8f71d4d 100644
--- a/cros-disks/rar_manager_test.cc
+++ b/cros-disks/rar_manager_test.cc
@@ -14,18 +14,6 @@
 
 namespace cros_disks {
 
-std::ostream& operator<<(std::ostream& out,
-                         const FUSEMounterLegacy::BindPath& x) {
-  return out << "{ path: " << quote(x.path) << ", writable: " << x.writable
-             << ", recursive: " << x.recursive << " }";
-}
-
-bool operator==(const FUSEMounterLegacy::BindPath& a,
-                const FUSEMounterLegacy::BindPath& b) {
-  return a.path == b.path && a.writable == b.writable &&
-         a.recursive == b.recursive;
-}
-
 namespace {
 
 using ::testing::_;
@@ -221,26 +209,23 @@
 
 TEST_F(RarManagerTest, GetBindPathsWithOldNamingScheme) {
   const RarManager& m = manager_;
-  EXPECT_THAT(m.GetBindPaths("poi"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>({{"poi"}}));
+  EXPECT_THAT(m.GetBindPaths("poi"), ElementsAreArray<std::string>({"poi"}));
 
   EXPECT_CALL(platform_, PathExists("poi.r00")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("poi.rar"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>({{"poi.rar"}}));
+              ElementsAreArray<std::string>({"poi.rar"}));
 
   EXPECT_CALL(platform_, PathExists("poi.r00")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("poi.r01")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("poi.r02")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("poi.rar"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>(
-                  {{"poi.rar"}, {"poi.r00"}, {"poi.r01"}}));
+              ElementsAreArray<std::string>({"poi.rar", "poi.r00", "poi.r01"}));
 
   EXPECT_CALL(platform_, PathExists("POI.R00")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("POI.R01")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("POI.R02")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("POI.RAR"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>(
-                  {{"POI.RAR"}, {"POI.R00"}, {"POI.R01"}}));
+              ElementsAreArray<std::string>({"POI.RAR", "POI.R00", "POI.R01"}));
 }
 
 TEST_F(RarManagerTest, GetBindPathsWithNewNamingScheme) {
@@ -248,7 +233,7 @@
 
   EXPECT_CALL(platform_, PathExists("poi1.rar")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("poi2.rar"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>({{"poi2.rar"}}));
+              ElementsAreArray<std::string>({"poi2.rar"}));
 
   EXPECT_CALL(platform_, PathExists("poi1.rar")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("poi2.rar")).WillOnce(Return(true));
@@ -256,8 +241,8 @@
   EXPECT_CALL(platform_, PathExists("poi4.rar")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("poi5.rar")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("poi2.rar"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>(
-                  {{"poi2.rar"}, {"poi1.rar"}, {"poi3.rar"}, {"poi4.rar"}}));
+              ElementsAreArray<std::string>(
+                  {"poi2.rar", "poi1.rar", "poi3.rar", "poi4.rar"}));
 
   EXPECT_CALL(platform_, PathExists("POI1.RAR")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("POI2.RAR")).WillOnce(Return(true));
@@ -265,8 +250,8 @@
   EXPECT_CALL(platform_, PathExists("POI4.RAR")).WillOnce(Return(true));
   EXPECT_CALL(platform_, PathExists("POI5.RAR")).WillOnce(Return(false));
   EXPECT_THAT(m.GetBindPaths("POI2.RAR"),
-              ElementsAreArray<FUSEMounterLegacy::BindPath>(
-                  {{"POI2.RAR"}, {"POI1.RAR"}, {"POI3.RAR"}, {"POI4.RAR"}}));
+              ElementsAreArray<std::string>(
+                  {"POI2.RAR", "POI1.RAR", "POI3.RAR", "POI4.RAR"}));
 }
 
 TEST_F(RarManagerTest, GetBindPathsStopsOnOverflow) {
diff --git a/cros-disks/zip_manager.cc b/cros-disks/zip_manager.cc
index 4f14425..d7fb47f 100644
--- a/cros-disks/zip_manager.cc
+++ b/cros-disks/zip_manager.cc
@@ -11,6 +11,7 @@
 #include <base/logging.h>
 #include <base/strings/string_util.h>
 
+#include "cros-disks/archive_mounter.h"
 #include "cros-disks/error_logger.h"
 #include "cros-disks/fuse_mounter.h"
 #include "cros-disks/metrics.h"
@@ -19,15 +20,53 @@
 
 namespace cros_disks {
 
+namespace {
+
+std::unique_ptr<ArchiveMounter> CreateZipMounter(
+    Platform* platform,
+    Metrics* metrics,
+    brillo::ProcessReaper* process_reaper,
+    std::vector<gid_t> supplementary_groups) {
+  OwnerUser run_as;
+  PCHECK(platform->GetUserAndGroupId("fuse-zip", &run_as.uid, &run_as.gid))
+      << "Cannot resolve required user fuse-zip";
+
+  const SandboxedExecutable executable = {
+      base::FilePath("/usr/bin/fuse-zip"),
+      base::FilePath("/usr/share/policy/fuse-zip-seccomp.policy")};
+
+  auto sandbox_factory = std::make_unique<FUSESandboxedProcessFactory>(
+      platform, std::move(executable), std::move(run_as),
+      /* has_network_access= */ false, std::move(supplementary_groups));
+
+  std::vector<int> password_needed_codes = {
+      23,   // ZIP_ER_BASE + ZIP_ER_ZLIB
+      36,   // ZIP_ER_BASE + ZIP_ER_NOPASSWD
+      37};  // ZIP_ER_BASE + ZIP_ER_WRONGPASSWD
+
+  return std::make_unique<ArchiveMounter>(
+      platform, process_reaper, "zip", metrics, "FuseZip",
+      std::move(password_needed_codes), std::move(sandbox_factory));
+}
+
+}  // namespace
+
+ZipManager::ZipManager(const std::string& mount_root,
+                       Platform* platform,
+                       Metrics* metrics,
+                       brillo::ProcessReaper* process_reaper)
+    : ArchiveManager(mount_root, platform, metrics, process_reaper),
+      mounter_(CreateZipMounter(
+          platform, metrics, process_reaper, GetSupplementaryGroups())) {}
+
 ZipManager::~ZipManager() {
   UnmountAll();
 }
 
 bool ZipManager::CanMount(const std::string& source_path) const {
-  // Check for expected file extension.
-  return base::EndsWith(source_path, ".zip",
-                        base::CompareCase::INSENSITIVE_ASCII) &&
-         IsInAllowedFolder(source_path);
+  base::FilePath name;
+  return IsInAllowedFolder(source_path) &&
+         mounter_->CanMount(source_path, {}, &name);
 }
 
 std::unique_ptr<MountPoint> ZipManager::DoMount(
@@ -37,11 +76,7 @@
     const base::FilePath& mount_path,
     MountOptions* const applied_options,
     MountErrorType* const error) {
-  DCHECK(applied_options);
   DCHECK(error);
-
-  metrics()->RecordArchiveType("zip");
-
   // MountManager resolves source path to real path before calling DoMount,
   // so no symlinks or '..' will be here.
   if (!IsInAllowedFolder(source_path)) {
@@ -49,34 +84,7 @@
     *error = MOUNT_ERROR_INVALID_DEVICE_PATH;
     return nullptr;
   }
-
-  FUSEMounterLegacy::Params params{
-      .bind_paths = {{source_path}},
-      .filesystem_type = "zipfs",
-      .metrics = metrics(),
-      .metrics_name = "FuseZip",
-      .mount_namespace = GetMountNamespaceFor(source_path).name,
-      .mount_program = "/usr/bin/fuse-zip",
-      .mount_user = "fuse-zip",
-      .password_needed_codes = {23,   // ZIP_ER_BASE + ZIP_ER_ZLIB
-                                36,   // ZIP_ER_BASE + ZIP_ER_NOPASSWD
-                                37},  // ZIP_ER_BASE + ZIP_ER_WRONGPASSWD
-      .platform = platform(),
-      .process_reaper = process_reaper(),
-      .seccomp_policy = "/usr/share/policy/fuse-zip-seccomp.policy",
-      .supplementary_groups = GetSupplementaryGroups(),
-  };
-
-  // Prepare FUSE mount options.
-  *error = GetMountOptions(&params.mount_options);
-  if (*error != MOUNT_ERROR_NONE)
-    return nullptr;
-
-  *applied_options = params.mount_options;
-
-  // Run fuse-zip.
-  const FUSEMounterLegacy mounter(std::move(params));
-  return mounter.Mount(source_path, mount_path, options, error);
+  return mounter_->Mount(source_path, mount_path, options, error);
 }
 
 }  // namespace cros_disks
diff --git a/cros-disks/zip_manager.h b/cros-disks/zip_manager.h
index 0d9cecd..18981d2 100644
--- a/cros-disks/zip_manager.h
+++ b/cros-disks/zip_manager.h
@@ -13,10 +13,17 @@
 
 namespace cros_disks {
 
+class ArchiveMounter;
+
 // A MountManager mounting ZIP archives as virtual filesystems using fuse-zip.
 class ZipManager : public ArchiveManager {
  public:
-  using ArchiveManager::ArchiveManager;
+  ZipManager(const std::string& mount_root,
+             Platform* platform,
+             Metrics* metrics,
+             brillo::ProcessReaper* process_reaper);
+  ZipManager(const ZipManager&) = delete;
+  ZipManager& operator=(const ZipManager&) = delete;
 
   ~ZipManager() override;
 
@@ -30,6 +37,8 @@
                                       const base::FilePath& mount_path,
                                       MountOptions* applied_options,
                                       MountErrorType* error) override;
+
+  const std::unique_ptr<ArchiveMounter> mounter_;
 };
 
 }  // namespace cros_disks
diff --git a/cros-disks/zip_manager_test.cc b/cros-disks/zip_manager_test.cc
index b26ac8b..4517cc2 100644
--- a/cros-disks/zip_manager_test.cc
+++ b/cros-disks/zip_manager_test.cc
@@ -15,12 +15,28 @@
 
 const char kMountRootDirectory[] = "/my_mount_point";
 
+class MockPlatform : public Platform {
+ public:
+  bool GetUserAndGroupId(const std::string& name,
+                         uid_t* uid,
+                         gid_t* gid) const override {
+    if (name == "fuse-zip") {
+      if (uid)
+        *uid = 200;
+      if (gid)
+        *gid = 300;
+      return true;
+    }
+    return false;
+  }
+};
+
 }  // namespace
 
 class ZipManagerTest : public testing::Test {
  protected:
   Metrics metrics_;
-  Platform platform_;
+  MockPlatform platform_;
   brillo::ProcessReaper reaper_;
   const ZipManager manager_{kMountRootDirectory, &platform_, &metrics_,
                             &reaper_};