Package: schroot
Followup-For: Bug #728096

Users have hit this issue in Ubuntu, too. I've triaged it and have
determined that Laurent is correct in Message #17 that the schroot mount
helper is using the host's filesystem to resolve symlinks in the chroot.

It is simply calling realpath(3) on the mount destination in the chroot,
from a process that is not chrooted, so realpath(3) is going to use the
host filesystem to resolve symlinks such as
'/<chroot>/dev/shm -> /run/shm'. It then tries to fixup the final
resolved path but appended the chroot basepath but it's too late at that
point, the host's filesystem has already been used to resolve the
symlink target.

We're actually getting filesystems mounted in the host filesystem in
Ubuntu (LP: #1438942) due to /run/shm existing in the host and pointing
back to /dev/shm.

Here's a simple test:

  # Show that schroot's mount binary is using the host fs to resolve symlinks
  $ mkdir -p /tmp/bug728096/dev
  $ mkdir -p /tmp/bug728096/run/shm
  $ ln -s /run/shm /tmp/bug728096/dev/shm
  $ ls -ld /{dev,run}/shm
  drwxrwxrwt 2 root root 120 Jul 29 14:07 /dev/shm
  lrwxrwxrwx 1 root root   8 Jul 29 14:07 /run/shm -> /dev/shm
  $ ls -ld /tmp/bug728096/{dev,run}/shm
  lrwxrwxrwx 1 tyhicks tyhicks    8 Jul 29 14:49 /tmp/bug728096/dev/shm -> 
/run/shm
  drwxrwxr-x 2 tyhicks tyhicks 4096 Jul 29 14:49 /tmp/bug728096/run/shm
  $ echo "tmpfs /dev/shm/ tmpfs defaults 0 0" > /tmp/fstab
  $ grep /dev/shm /proc/mounts
  tmpfs /dev/shm tmpfs rw,nosuid,nodev 0 0
  $ sudo ./libexec/mount/mount -v -f /tmp/fstab -m /tmp/bug728096
  $ grep /dev/shm /proc/mounts
  tmpfs /dev/shm tmpfs rw,nosuid,nodev 0 0
  tmpfs /dev/shm tmpfs rw,relatime 0 0
  
  # Now reproduce Laurent's original bug report by removing the host's /run/shm
  $ sudo umount /dev/shm
  $ sudo rm /run/shm
  $ sudo ./libexec/mount/mount -v -f /tmp/fstab -m /tmp/bug728096
  E: boost::filesystem::create_directory: File exists: "/tmp/bug728096/dev/shm"
  $ echo $?
  1

I've attached fixes for the master and 1.6 schroot branches. They fork
off a process that changes root to the chroot base path before calling
realpath(3) on the mount destination.

Note that I'm still pretty confused by a part of the code in
resolve_path_chrooted() (which is mostly the old resolve_path()). I
don't understand the what is happening after the
"// Try validating the parent directory." comment but I left it since I
couldn't wrap my head around it. Any insight to what it is doing and
whether we can remove it now?

Tyler
From 86a39d878bc1b6ea59b4f354f03b635014b720a1 Mon Sep 17 00:00:00 2001
From: Tyler Hicks <tyhi...@canonical.com>
Date: Tue, 28 Jul 2015 02:11:07 -0500
Subject: [PATCH] libexec-mount: Resolve mount destinations while chrooted

The mount binary was attempting to use realpath(3) from outside of the
chroot to resolve mount destination paths inside of the chroot. It would
then take the resolved path and prepend it with the path to the chroot
in an attempt to enforce that symlink resolution will always end up
inside of the chroot.

One example of why this approach isn't sufficient is that when
/<chroot>/dev/shm/ is the mount destination but it is a symlink to
/run/shm and the host's /run/shm is a symlink to /dev/shm. The resolved
path will end up being /<chroot>/dev/shm/ and, due to mount following
symlinks, the host's /dev/shm will be mounted over.

To fix the path resolution issue, this patch first resolves the path to
the chroot base path, then forks and calls chroot(2) on that path, then
resolves the path to the mount destination inside the chroot. Finally,
the resolved chroot base path and the resolved mount destination path
are combined to create the fully resolved path used for mounting.

Bug-Debian: https://bugs.debian.org/728096
Bug-Ubuntu: https://launchpad.net/bugs/1438942

Signed-off-by: Tyler Hicks <tyhi...@canonical.com>
---
 libexec/mount/main.cc | 141 ++++++++++++++++++++++++++++++++++++++++++--------
 libexec/mount/main.h  |  28 ++++++++--
 2 files changed, 141 insertions(+), 28 deletions(-)

diff --git a/libexec/mount/main.cc b/libexec/mount/main.cc
index 9ea6e80..bbf4a18 100644
--- a/libexec/mount/main.cc
+++ b/libexec/mount/main.cc
@@ -30,6 +30,7 @@
 #include <ctime>
 #include <iostream>
 #include <locale>
+#include <poll.h>
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -53,8 +54,14 @@ namespace schroot
       {
         {bin::schroot_mount::main::CHILD_FORK, N_("Failed to fork child")},
         {bin::schroot_mount::main::CHILD_WAIT, N_("Wait for child failed")},
+        // TRANSLATORS: %1% = directory
+        {bin::schroot_mount::main::CHROOT,     N_("Failed to change root to directory ‘%1%’")},
+        {bin::schroot_mount::main::DUP,        N_("Failed to duplicate file descriptor")},
         // TRANSLATORS: %1% = command name
         {bin::schroot_mount::main::EXEC,       N_("Failed to execute “%1%”")},
+        {bin::schroot_mount::main::PIPE,       N_("Failed to create pipe")},
+        {bin::schroot_mount::main::POLL,       N_("Failed to poll file descriptor")},
+        {bin::schroot_mount::main::READ,       N_("Failed to read file descriptor")},
         {bin::schroot_mount::main::REALPATH,   N_("Failed to resolve path “%1%”")}
       };
 
@@ -81,23 +88,14 @@ namespace bin
     }
 
     std::string
-    main::resolve_path (const std::string& mountpoint)
+    main::resolve_path_chrooted (const std::string& mountpoint)
     {
-      // Ensure entry has a leading / to prevent security hole where
-      // mountpoint might be outside the chroot.
-      std::string absmountpoint(mountpoint);
-      if (absmountpoint.empty() || absmountpoint[0] != '/')
-        absmountpoint = std::string("/") + absmountpoint;
-
-      char *resolved_path = realpath(opts->mountpoint.c_str(), 0);
-      if (!resolved_path)
-        throw error(opts->mountpoint, REALPATH, strerror(errno));
-      std::string basepath(resolved_path);
-      std::free(resolved_path);
-
-      std::string directory(opts->mountpoint + absmountpoint);
+      std::string directory(mountpoint);
+      if (directory.empty() || directory[0] != '/')
+        directory = std::string("/") + directory;
+
       // Canonicalise path to remove any symlinks.
-      resolved_path = realpath(directory.c_str(), 0);
+      char *resolved_path = realpath(directory.c_str(), 0);
       if (resolved_path == 0)
         {
           // The path is either not present or is an invalid link.  If
@@ -117,13 +115,13 @@ namespace bin
           else
             {
               // Try validating the parent directory.
-              schroot::string_list dirs = schroot::split_string(mountpoint, "/");
+              schroot::string_list dirs = schroot::split_string(directory, "/");
               if (dirs.size() > 1) // Recurse if possible, otherwise continue
                 {
                   std::string saveddir = *dirs.rbegin();
                   dirs.pop_back();
 
-                  std::string newpath(resolve_path(schroot::string_list_to_string(dirs, "/")));
+                  std::string newpath(resolve_path_chrooted(schroot::string_list_to_string(dirs, "/")));
                   directory = newpath + "/" + saveddir;
                 }
             }
@@ -133,16 +131,113 @@ namespace bin
           directory = resolved_path;
           std::free(resolved_path);
         }
-      // If the link was absolute (i.e. points somewhere on the host,
-      // outside the chroot, make sure that this is modified to be
-      // inside.
-      if (directory.size() < basepath.size() ||
-          directory.substr(0,basepath.size()) != basepath)
-        directory = basepath + directory;
 
       return directory;
     }
 
+    std::string
+    main::resolve_path (const std::string& mountpoint)
+    {
+      std::string stdout_buf;
+      int stdout_pipe[2];
+      int exit_status = 0;
+      pid_t pid;
+
+      try
+        {
+          if (pipe(stdout_pipe) < 0)
+            throw error(PIPE, strerror(errno));
+
+          if ((pid = fork()) == -1)
+            {
+              throw error(CHILD_FORK, strerror(errno));
+            }
+          else if (pid == 0)
+            {
+              try
+                {
+                  // Set up pipes for stdout
+                  if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
+                    throw error(DUP, strerror(errno));
+
+                  close(stdout_pipe[0]);
+                  close(stdout_pipe[1]);
+
+                  char *resolved_path = realpath(opts->mountpoint.c_str(), 0);
+                  if (!resolved_path)
+                    throw error(opts->mountpoint, REALPATH, strerror(errno));
+
+                  std::string basepath(resolved_path);
+                  std::free(resolved_path);
+
+                  if (chroot(basepath.c_str()) < 0)
+                    throw error(basepath, CHROOT, strerror(errno));
+
+                  std::cout << basepath + resolve_path_chrooted(mountpoint);
+                  std::cout.flush();
+                  _exit(EXIT_SUCCESS);
+                }
+              catch (std::exception const& e)
+                {
+                  schroot::log_exception_error(e);
+                }
+              catch (...)
+                {
+                  schroot::log_error()
+                    << _("An unknown exception occurred") << std::endl;
+                }
+              _exit(EXIT_FAILURE);
+            }
+
+          // Log stdout
+          close(stdout_pipe[1]);
+
+          struct pollfd pollfd;
+          pollfd.fd = stdout_pipe[0];
+          pollfd.events = POLLIN;
+          pollfd.revents = 0;
+
+          char buffer[BUFSIZ];
+
+          while (1)
+            {
+              int status;
+
+              if ((status = poll(&pollfd, 1, -1)) < 0)
+                throw error(POLL, strerror(errno));
+
+              int outdata = 0;
+
+              if (pollfd.revents & POLLIN)
+                {
+                  if ((outdata = read(pollfd.fd, buffer, BUFSIZ)) < 0
+                      && errno != EINTR)
+                    throw error(READ, strerror(errno));
+
+                  if (outdata)
+                    stdout_buf += std::string(&buffer[0], outdata);
+                }
+
+              if (outdata == 0) // pipe closed
+                break;
+            }
+
+          close(stdout_pipe[0]);
+          wait_for_child(pid, exit_status);
+        }
+      catch (error const& e)
+        {
+          close(stdout_pipe[0]);
+          close(stdout_pipe[1]);
+          throw;
+        }
+
+      if (exit_status)
+        exit(exit_status);
+
+      return stdout_buf;
+    }
+
     void
     main::action_mount ()
     {
diff --git a/libexec/mount/main.h b/libexec/mount/main.h
index c523bfe..790dc3a 100644
--- a/libexec/mount/main.h
+++ b/libexec/mount/main.h
@@ -45,7 +45,12 @@ namespace bin
         {
           CHILD_FORK, ///< Failed to fork child.
           CHILD_WAIT, ///< Wait for child failed.
+          CHROOT,     ///< Failed to change root.
+          DUP,        ///< Failed to duplicate file descriptor.
           EXEC,       ///< Failed to execute.
+          PIPE,       ///< Failed to create pipe.
+          POLL,       ///< Failed to poll file descriptor.
+          READ,       ///< Failed to read file descriptor.
           REALPATH    ///< Failed to resolve path.
         };
 
@@ -84,16 +89,29 @@ namespace bin
                 const schroot::environment& env);
 
       /**
-       * Ensure that the mountpoint is a valid absolute path inside the
-       * chroot.  This is to avoid absolute or relative symlinks
-       * pointing outside the chroot causing filesystems to be mounted
-       * on the host.  An exception will be thrown if it is not possible
-       * to resolve the path.
+       * Ensure that the mountpoint is a valid absolute path. The calling
+       * process must be chrooted before calling this function to avoid
+       * resolving absolute or relative symlinks pointing outside the chroot.
+       * An exception will be thrown if it is not possible to resolve the path.
        *
        * @param mountpoint the mountpoint to check,
        * @returns the validated path.
        */
+      std::string
+      resolve_path_chrooted (const std::string& mountpoint);
 
+      /**
+       * Ensure that the chroot base path and the mountpoint is a valid
+       * absolute path inside the chroot.  This function calls chroot on the
+       * resolved chroot base path before attempting to resolve the mountpoint
+       * path inside of the chroot to avoid absolute or relative symlinks
+       * pointing outside the chroot causing filesystems to be mounted on the
+       * host.  An exception will be thrown if it is not possible to resolve
+       * the chroot base path or the mountpoint path.
+       *
+       * @param mountpoint the mountpoint to check,
+       * @returns the validated path.
+       */
       std::string
       resolve_path (const std::string& mountpoint);
 
-- 
2.1.4

From c051ee6ef19d0592baac099c19dc485b42bd1771 Mon Sep 17 00:00:00 2001
From: Tyler Hicks <tyhi...@canonical.com>
Date: Tue, 28 Jul 2015 01:01:12 -0500
Subject: [PATCH] schroot-mount: Resolve mount destinations while chrooted

The schroot-mount binary was attempting to use realpath(3) from outside
of the chroot to resolve mount destination paths inside of the chroot.
It would then take the resolved path and prepend it with the path to the
chroot in an attempt to enforce that symlink resolution will always end
up inside of the chroot.

One example of why this approach isn't sufficient is that when
/<chroot>/dev/shm/ is the mount destination but it is a symlink to
/run/shm and the host's /run/shm is a symlink to /dev/shm. The resolved
path will end up being /<chroot>/dev/shm/ and, due to mount following
symlinks, the host's /dev/shm will be mounted over.

To fix the path resolution issue, this patch first resolves the path to
the chroot base path, then forks and calls chroot(2) on that path, then
resolves the path to the mount destination inside the chroot. Finally,
the resolved chroot base path and the resolved mount destination path
are combined to create the fully resolved path used for mounting.

Bug-Debian: https://bugs.debian.org/728096
Bug-Ubuntu: https://launchpad.net/bugs/1438942

Signed-off-by: Tyler Hicks <tyhi...@canonical.com>
---
 bin/schroot-mount/schroot-mount-main.cc | 141 ++++++++++++++++++++++++++------
 bin/schroot-mount/schroot-mount-main.h  |  29 +++++--
 2 files changed, 142 insertions(+), 28 deletions(-)

diff --git a/bin/schroot-mount/schroot-mount-main.cc b/bin/schroot-mount/schroot-mount-main.cc
index 327f5ed..aa5004b 100644
--- a/bin/schroot-mount/schroot-mount-main.cc
+++ b/bin/schroot-mount/schroot-mount-main.cc
@@ -30,6 +30,7 @@
 #include <ctime>
 #include <iostream>
 #include <locale>
+#include <poll.h>
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -59,8 +60,14 @@ namespace
     {
       emap(main::CHILD_FORK, N_("Failed to fork child")),
       emap(main::CHILD_WAIT, N_("Wait for child failed")),
+      // TRANSLATORS: %1% = directory
+      emap(main::CHROOT,     N_("Failed to change root to directory ‘%1%’")),
+      emap(main::DUP,        N_("Failed to duplicate file descriptor")),
       // TRANSLATORS: %1% = command name
       emap(main::EXEC,       N_("Failed to execute “%1%”")),
+      emap(main::PIPE,       N_("Failed to create pipe")),
+      emap(main::POLL,       N_("Failed to poll file descriptor")),
+      emap(main::READ,       N_("Failed to read file descriptor")),
       emap(main::REALPATH,   N_("Failed to resolve path “%1%”"))
     };
 
@@ -88,23 +95,14 @@ main::~main ()
 }
 
 std::string
-main::resolve_path (std::string const& mountpoint)
+main::resolve_path_chrooted (std::string const& mountpoint)
 {
-  // Ensure entry has a leading / to prevent security hole where
-  // mountpoint might be outside the chroot.
-  std::string absmountpoint(mountpoint);
-  if (absmountpoint.empty() || absmountpoint[0] != '/')
-    absmountpoint = std::string("/") + absmountpoint;
-
-  char *resolved_path = realpath(opts->mountpoint.c_str(), 0);
-  if (!resolved_path)
-    throw error(opts->mountpoint, REALPATH, strerror(errno));
-  std::string basepath(resolved_path);
-  std::free(resolved_path);
-
-  std::string directory(opts->mountpoint + absmountpoint);
+  std::string directory(mountpoint);
+  if (directory.empty() || directory[0] != '/')
+    directory = std::string("/") + directory;
+
   // Canonicalise path to remove any symlinks.
-  resolved_path = realpath(directory.c_str(), 0);
+  char *resolved_path = realpath(directory.c_str(), 0);
   if (resolved_path == 0)
     {
       // The path is either not present or is an invalid link.  If
@@ -124,13 +122,13 @@ main::resolve_path (std::string const& mountpoint)
       else
         {
           // Try validating the parent directory.
-          sbuild::string_list dirs = sbuild::split_string(mountpoint, "/");
+          sbuild::string_list dirs = sbuild::split_string(directory, "/");
           if (dirs.size() > 1) // Recurse if possible, otherwise continue
             {
               std::string saveddir = *dirs.rbegin();
               dirs.pop_back();
 
-              std::string newpath(resolve_path(sbuild::string_list_to_string(dirs, "/")));
+              std::string newpath(resolve_path_chrooted(sbuild::string_list_to_string(dirs, "/")));
               directory = newpath + "/" + saveddir;
             }
         }
@@ -140,16 +138,113 @@ main::resolve_path (std::string const& mountpoint)
       directory = resolved_path;
       std::free(resolved_path);
     }
-  // If the link was absolute (i.e. points somewhere on the host,
-  // outside the chroot, make sure that this is modified to be
-  // inside.
-  if (directory.size() < basepath.size() ||
-      directory.substr(0,basepath.size()) != basepath)
-    directory = basepath + directory;
 
   return directory;
 }
 
+std::string
+main::resolve_path (std::string const& mountpoint)
+{
+  std::string stdout_buf;
+  int stdout_pipe[2];
+  int exit_status = 0;
+  pid_t pid;
+
+  try
+    {
+      if (pipe(stdout_pipe) < 0)
+        throw error(PIPE, strerror(errno));
+
+      if ((pid = fork()) == -1)
+        {
+          throw error(CHILD_FORK, strerror(errno));
+        }
+      else if (pid == 0)
+        {
+          try
+            {
+              // Set up pipes for stdout
+              if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
+                throw error(DUP, strerror(errno));
+
+              close(stdout_pipe[0]);
+              close(stdout_pipe[1]);
+
+              char *resolved_path = realpath(opts->mountpoint.c_str(), 0);
+              if (!resolved_path)
+                throw error(opts->mountpoint, REALPATH, strerror(errno));
+
+              std::string basepath(resolved_path);
+              std::free(resolved_path);
+
+              if (chroot(basepath.c_str()) < 0)
+                throw error(basepath, CHROOT, strerror(errno));
+
+              std::cout << basepath + resolve_path_chrooted(mountpoint);
+              std::cout.flush();
+              _exit(EXIT_SUCCESS);
+            }
+          catch (std::exception const& e)
+            {
+              sbuild::log_exception_error(e);
+            }
+          catch (...)
+            {
+              sbuild::log_error()
+                << _("An unknown exception occurred") << std::endl;
+            }
+          _exit(EXIT_FAILURE);
+        }
+
+      // Log stdout
+      close(stdout_pipe[1]);
+
+      struct pollfd pollfd;
+      pollfd.fd = stdout_pipe[0];
+      pollfd.events = POLLIN;
+      pollfd.revents = 0;
+
+      char buffer[BUFSIZ];
+
+      while (1)
+        {
+          int status;
+
+          if ((status = poll(&pollfd, 1, -1)) < 0)
+            throw error(POLL, strerror(errno));
+
+          int outdata = 0;
+
+          if (pollfd.revents & POLLIN)
+            {
+              if ((outdata = read(pollfd.fd, buffer, BUFSIZ)) < 0
+                  && errno != EINTR)
+                throw error(READ, strerror(errno));
+
+              if (outdata)
+                stdout_buf += std::string(&buffer[0], outdata);
+            }
+
+          if (outdata == 0) // pipe closed
+            break;
+        }
+
+      close(stdout_pipe[0]);
+      wait_for_child(pid, exit_status);
+    }
+  catch (error const& e)
+    {
+      close(stdout_pipe[0]);
+      close(stdout_pipe[1]);
+      throw;
+    }
+
+  if (exit_status)
+    exit(exit_status);
+
+  return stdout_buf;
+}
+
 void
 main::action_mount ()
 {
diff --git a/bin/schroot-mount/schroot-mount-main.h b/bin/schroot-mount/schroot-mount-main.h
index 06c0333..7b0ef24 100644
--- a/bin/schroot-mount/schroot-mount-main.h
+++ b/bin/schroot-mount/schroot-mount-main.h
@@ -42,7 +42,12 @@ namespace schroot_mount
       {
         CHILD_FORK, ///< Failed to fork child.
         CHILD_WAIT, ///< Wait for child failed.
+        CHROOT,     ///< Failed to change root.
+        DUP,        ///< Failed to duplicate file descriptor.
         EXEC,       ///< Failed to execute.
+        PIPE,       ///< Failed to create pipe.
+        POLL,       ///< Failed to poll file descriptor.
+        READ,       ///< Failed to read file descriptor.
         REALPATH    ///< Failed to resolve path.
       };
 
@@ -81,17 +86,31 @@ namespace schroot_mount
               sbuild::environment const& env);
 
     /**
-     * Ensure that the mountpoint is a valid absolute path inside the
-     * chroot.  This is to avoid absolute or relative symlinks
-     * pointing outside the chroot causing filesystems to be mounted
-     * on the host.  An exception will be thrown if it is not possible
-     * to resolve the path.
+     * Ensure that the mountpoint is a valid absolute path. The calling process
+     * must be chrooted before calling this function to avoid resolving
+     * absolute or relative symlinks pointing outside the chroot.  An exception
+     * will be thrown if it is not possible to resolve the path.
      *
      * @param mountpoint the mountpoint to check,
      * @returns the validated path.
      */
 
     std::string
+    resolve_path_chrooted (std::string const& mountpoint);
+
+    /**
+     * Ensure that the chroot base path and the mountpoint is a valid absolute
+     * path inside the chroot.  This function calls chroot on the resolved
+     * chroot base path before attempting to resolve the mountpoint path inside
+     * of the chroot to avoid absolute or relative symlinks pointing outside
+     * the chroot causing filesystems to be mounted on the host.  An exception
+     * will be thrown if it is not possible to resolve the chroot base path or
+     * the mountpoint path.
+     *
+     * @param mountpoint the mountpoint to check,
+     * @returns the validated path.
+     */
+    std::string
     resolve_path (std::string const& mountpoint);
 
     /**
-- 
2.1.4

Attachment: signature.asc
Description: Digital signature

Reply via email to