login: Explicitly remount the container with MS_RDONLY

Turns out that even if the filesystem is mounted read-only outside the
container, the same mount inside the container will preserve the flag
that was passed explicitly (0, meaning read-write), even if the
underlying filesystem was marked read-only.

This change explicitly remounts the containers' rootfs with MS_RDONLY if
the underlying filesystem is mounted read-only.

BUG=b:31771616
TEST=FEATURES="test" emerge-${BOARD} chromeos-base/chromeos-login
TEST=test_that cheets_FileSystemPermission  # passes
TEST=test_that cheets_FileSystemPermission  # fails with WRITABLE_MOUNT=1

Change-Id: I1d715c22779226937546e0566dceb0de29501b60
Reviewed-on: https://chromium-review.googlesource.com/390412
Commit-Ready: Luis Hector Chavez <lhchavez@google.com>
Tested-by: Luis Hector Chavez <lhchavez@google.com>
Reviewed-by: Luis Hector Chavez <lhchavez@google.com>
(cherry picked from commit debfbb561c97861f211c877d9de233bd67d5a313)
Reviewed-on: https://chromium-review.googlesource.com/392348
Trybot-Ready: Luis Hector Chavez <lhchavez@google.com>
Reviewed-by: Dan Erat <derat@chromium.org>
Commit-Queue: Luis Hector Chavez <lhchavez@google.com>
diff --git a/login_manager/container_config_parser.cc b/login_manager/container_config_parser.cc
index 80ce7b8..eb0d93e 100644
--- a/login_manager/container_config_parser.cc
+++ b/login_manager/container_config_parser.cc
@@ -9,10 +9,15 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <algorithm>
+#include <string>
 #include <vector>
 
 #include <base/files/file_path.h>
+#include <base/files/file_util.h>
 #include <base/json/json_reader.h>
+#include <base/strings/string_split.h>
+#include <base/strings/string_util.h>
 #include <base/values.h>
 
 #include <libcontainer/libcontainer.h>
@@ -21,10 +26,48 @@
 
 namespace {
 
+// Parses |mountinfo_data| (the contents of /proc/self/mountinfo) to determine
+// whether |rootfs_path| was originally mounted as read-only.
+bool IsOriginalRootfsReadOnly(const std::string& mountinfo_data,
+                              const base::FilePath& rootfs_path) {
+  constexpr size_t kMountinfoMountPointIndex = 4;
+  constexpr size_t kMountinfoMountOptionsIndex = 5;
+  const size_t kMountinfoMinNumberOfTokens =
+      std::max(kMountinfoMountPointIndex, kMountinfoMountOptionsIndex) + 1;
+
+  std::vector<std::string> lines =
+      base::SplitString(mountinfo_data, "\n", base::TRIM_WHITESPACE,
+                        base::SPLIT_WANT_NONEMPTY);
+
+  for (const auto& line : lines) {
+    std::vector<std::string> tokens =
+        base::SplitString(line, base::kWhitespaceASCII, base::TRIM_WHITESPACE,
+                          base::SPLIT_WANT_NONEMPTY);
+    // Some fields in /proc/self/mountinfo are optional. We only need the line
+    // to have |kMountinfoMinNumberOfTokens|.
+    if (tokens.size() < kMountinfoMinNumberOfTokens)
+      continue;
+    if (tokens[kMountinfoMountPointIndex] != rootfs_path.value())
+      continue;
+
+    std::vector<std::string> options =
+        base::SplitString(tokens[kMountinfoMountOptionsIndex], ",",
+                          base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
+
+    return std::find(options.begin(), options.end(), "ro") != options.end();
+  }
+
+  LOG(WARNING) << "Did not find mount information for " << rootfs_path.value()
+               << ". Assuming mounted read-only.";
+
+  return true;
+}
+
 // Sets the rootfs of |config| to point to where the rootfs of the container
 // is mounted.
 bool ParseRootFileSystemConfig(const base::DictionaryValue& config_root_dict,
                                const base::FilePath& named_path,
+                               const std::string& mountinfo_data,
                                ContainerConfigPtr* config_out) {
   // |rootfs_dict| stays owned by |config_root_dict|
   const base::DictionaryValue* rootfs_dict = nullptr;
@@ -39,18 +82,20 @@
   }
   container_config_rootfs(config_out->get(),
                           named_path.Append(rootfs_path).value().c_str());
-  // Explicitly set the mount flags of the rootfs to 0.
+  // Explicitly set the mount flags of the rootfs.
   //
   // In Chrome OS, the rootfs is mounted nosuid, nodev, noexec. We need the
   // filesystem to be mounted without those three flags within the container for
-  // it to work correctly, so explicitly remount with no flags. Since the image
-  // is originally mounted through a loopback device, and loopback devices that
-  // are originally created as read-only cannot be set to writable after
-  // creation, not setting the MS_RDONLY flag is safe here and will preserve the
-  // read-only protection if the original filesystem is read-only, and will make
-  // it writable for developers that have manually made the original filesystem
-  // writable.
-  container_config_rootfs_mount_flags(config_out->get(), 0);
+  // it to work correctly, so explicitly remount with none of those flags. We
+  // need to preserve the ro/rw state of the original mount, though, since the
+  // internal namespace will reflect whatever flag was passed here instead of
+  // respecting the original filesystem's ro/rw state.
+  uint32_t flags = 0;
+  if (IsOriginalRootfsReadOnly(mountinfo_data,
+                               named_path.Append(rootfs_path))) {
+    flags |= MS_RDONLY;
+  }
+  container_config_rootfs_mount_flags(config_out->get(), flags);
   return true;
 }
 
@@ -458,10 +503,13 @@
 bool ParseConfigDicts(const base::DictionaryValue& config_root_dict,
                       const base::DictionaryValue& runtime_root_dict,
                       const base::FilePath& named_path,
+                      const std::string& mountinfo_data,
                       ContainerConfigPtr* config_out) {
   // Root fs info
-  if (!ParseRootFileSystemConfig(config_root_dict, named_path, config_out))
+  if (!ParseRootFileSystemConfig(config_root_dict, named_path,
+                                 mountinfo_data, config_out)) {
     return false;
+  }
 
   // Process info
   uid_t uid;
@@ -490,6 +538,7 @@
 
 bool ParseContainerConfig(const std::string& config_json_data,
                           const std::string& runtime_json_data,
+                          const std::string& mountinfo_data,
                           const std::string& container_name,
                           const std::string& parent_cgroup_name,
                           const base::FilePath& named_container_path,
@@ -522,8 +571,9 @@
     return false;
   }
   if (!ParseConfigDicts(*config_dict, *runtime_dict, named_container_path,
-                        config_out))
+                        mountinfo_data, config_out)) {
     return false;
+  }
 
   // Set the cgroup configuration
   if (container_config_set_cgroup_parent(
diff --git a/login_manager/container_config_parser.h b/login_manager/container_config_parser.h
index 84c504f..4c2b44a 100644
--- a/login_manager/container_config_parser.h
+++ b/login_manager/container_config_parser.h
@@ -5,8 +5,8 @@
 #ifndef LOGIN_MANAGER_CONTAINER_CONFIG_PARSER_H_
 #define LOGIN_MANAGER_CONTAINER_CONFIG_PARSER_H_
 
-#include <string>
 #include <memory>
+#include <string>
 
 #include <libcontainer/libcontainer.h>
 #include <base/files/file_path.h>
@@ -21,12 +21,14 @@
 // specified in https://github.com/opencontainers/runtime-spec/tree/v0.2.0
 //  |config_json_data| - The text from config.json.
 //  |runtime_json_data| - The text from runtime.json.
+//  |mountinfo_data| - The text from /proc/self/mountinfo.
 //  |container_name| - Unique name for the container.
 //  |parent_cgroup_name| - Name of the parent cgroup for this container.
 //  |named_container_path| - Path to the base of the container data and rootfs.
 //  |config_out| - Filled with the configuration, defined in libcontainer.
 bool ParseContainerConfig(const std::string& config_json_data,
                           const std::string& runtime_json_data,
+                          const std::string& mountinfo_data,
                           const std::string& container_name,
                           const std::string& parent_cgroup_name,
                           const base::FilePath& named_container_path,
diff --git a/login_manager/container_config_parser_unittest.cc b/login_manager/container_config_parser_unittest.cc
index f76d9b8..c885c51 100644
--- a/login_manager/container_config_parser_unittest.cc
+++ b/login_manager/container_config_parser_unittest.cc
@@ -148,6 +148,21 @@
 
 const char kCgroupParent[] = "test_cgroup";
 
+const char kTestcMountinfo[] = R"(
+104 99 0:57 / /proc rw,relatime - proc none rw
+73 15 7:1 /container.bin /var/run/containers/testc/rootfs_path ro,relatime -
+)";
+
+const char kAndroidReadonlyMountinfo[] = R"(
+104 99 0:57 / /proc rw,relatime - proc none rw
+73 15 7:1 /container.bin /var/run/containers/android/rootfs_path ro,relatime -
+)";
+
+const char kAndroidWritableMountinfo[] = R"(
+104 99 0:57 / /proc rw,relatime - proc none rw
+73 15 7:1 /container.bin /var/run/containers/android/rootfs_path rw,relatime -
+)";
+
 }  // anonymous namespace
 
 namespace login_manager {
@@ -158,7 +173,8 @@
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
   EXPECT_TRUE(ParseContainerConfig(kBasicJsonConfigData,
-                                   kBasicJsonRuntimeData, "testc",
+                                   kBasicJsonRuntimeData,
+                                   kTestcMountinfo, "testc",
                                    kCgroupParent,
                                    kNamedContainerPath, &config));
   EXPECT_EQ(kNamedContainerPath.Append("rootfs_path").value(),
@@ -185,7 +201,8 @@
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
   EXPECT_TRUE(ParseContainerConfig(kBasicJsonConfigData,
-                                   kBasicJsonRuntimeData, "android",
+                                   kBasicJsonRuntimeData,
+                                   kAndroidReadonlyMountinfo, "android",
                                    kCgroupParent,
                                    kNamedContainerPath, &config));
   EXPECT_EQ(kNamedContainerPath.Append("rootfs_path").value(),
@@ -197,6 +214,22 @@
             container_config_get_run_setfiles(config.get()));
   EXPECT_EQ(std::string(kCgroupParent),
             container_config_get_cgroup_parent(config.get()));
+  EXPECT_EQ(MS_BIND | MS_REMOUNT | MS_RDONLY,
+            container_config_get_rootfs_mount_flags(config.get()));
+}
+
+TEST(ContainerConfigParserTest, TestWritableMountConfigAndroid) {
+  const base::FilePath kNamedContainerPath("/var/run/containers/android");
+
+  ContainerConfigPtr config(container_config_create(),
+                            &container_config_destroy);
+  EXPECT_TRUE(ParseContainerConfig(kBasicJsonConfigData,
+                                   kBasicJsonRuntimeData,
+                                   kAndroidWritableMountinfo, "android",
+                                   kCgroupParent,
+                                   kNamedContainerPath, &config));
+  EXPECT_EQ(MS_BIND | MS_REMOUNT,
+            container_config_get_rootfs_mount_flags(config.get()));
 }
 
 TEST(ContainerConfigParserTest, TestFailedConfigRootDictEmpty) {
@@ -206,7 +239,8 @@
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
   EXPECT_FALSE(ParseContainerConfig(kEmptyJsonConfigData,
-                                    kBasicJsonRuntimeData, "testc",
+                                    kBasicJsonRuntimeData,
+                                    kTestcMountinfo, "testc",
                                     kCgroupParent,
                                     kNamedContainerPath, &config));
 }
@@ -218,7 +252,8 @@
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
   EXPECT_FALSE(ParseContainerConfig(kExtraMountJsonConfigData,
-                                    kBasicJsonRuntimeData, "testc",
+                                    kBasicJsonRuntimeData,
+                                    kTestcMountinfo, "testc",
                                     kCgroupParent,
                                     kNamedContainerPath, &config));
 }
@@ -229,7 +264,8 @@
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
   EXPECT_TRUE(ParseContainerConfig(kBasicJsonConfigData,
-                                   kCpuCgroupJsonRuntimeData, "testc",
+                                   kCpuCgroupJsonRuntimeData,
+                                   kTestcMountinfo, "testc",
                                    kCgroupParent,
                                    kNamedContainerPath, &config));
   EXPECT_EQ(1024, container_config_get_cpu_shares(config.get()));
diff --git a/login_manager/container_manager_impl.cc b/login_manager/container_manager_impl.cc
index d6e67f7..898ecaa 100644
--- a/login_manager/container_manager_impl.cc
+++ b/login_manager/container_manager_impl.cc
@@ -33,6 +33,7 @@
 const char kCpuSharesFile[] =
     "/sys/fs/cgroup/cpu/session_manager_containers/cpu.shares";
 const char kCpuSharesDefault[] = "1024";
+const char kMountinfoPath[] = "/proc/self/mountinfo";
 
 std::string libcontainer_strerror(int err) {
   if (err < 0) {
@@ -141,10 +142,17 @@
     return false;
   }
 
+  std::string mountinfo_data;
+  if (!base::ReadFileToString(base::FilePath(kMountinfoPath),
+                              &mountinfo_data)) {
+    LOG(WARNING) << "Fail to read mountinfo data from " << kMountinfoPath
+                 << ". Assuming all mounts are read-only.";
+  }
+
   ContainerConfigPtr config(container_config_create(),
                             &container_config_destroy);
-  if (!ParseContainerConfig(config_json_data, runtime_json_data, name_,
-                            kSessionManagerCgroup, container_directory_,
+  if (!ParseContainerConfig(config_json_data, runtime_json_data, mountinfo_data,
+                            name_, kSessionManagerCgroup, container_directory_,
                             &config)) {
     LOG(ERROR) << "Failed to parse container configuration for " << name_;
     return false;