Sandboxing (Linux)

There are multiple possibilities with Linux to sandbox applications.

  • chroot … Changed file system root
  • namespaces … Separated namespaces (e.g. Mount table)
  • cgroups … Restricted resources (e.g. Memory)

Example (with changed system root)

#include <filesystem>
#include <iostream>
#include <vector>

#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>

namespace sfs = std::filesystem;

void mountDeps(std::string& chrootPath, std::vector<std::string>& deps);
void unmountDeps(std::string& chrootPath, std::vector<std::string>& deps);
void startApp(std::string& chrootPath, std::vector<std::string>& deps);
void changeRoot(std::string& chrootPath);

int main()
{
  std::vector<std::string> deps = { "child.out", "/lib", "/lib64" };
  std::string chrootPath = "/tmp/chroot/";

  sfs::create_directory(chrootPath);
  mountDeps(chrootPath, deps);

  startApp(chrootPath, deps);

  unmountDeps(chrootPath, deps);
  sfs::remove_all(chrootPath);

  return 0;
}

void mountDeps(std::string& chrootPath, std::vector<std::string>& deps)
{
  for (std::string& dep : deps)
  {
    std::string dest = chrootPath + dep;

    if (sfs::is_directory(dep)) { sfs::create_directory(dest); }
    else { close(open(dest.c_str(), O_WRONLY | O_CREAT, 0755)); }

    if (mount(dep.c_str(), dest.c_str(), "", MS_BIND, ""))
    {
      std::cerr << "Bind mounting from " << dep << " to " << dest <<
                   " failed: " << strerror(errno) << std::endl;
    }
  }
}

void unmountDeps(std::string& chrootPath, std::vector<std::string>& deps)
{
  for (std::string& dep : deps)
  {
    std::string dest = chrootPath + dep;

    if (umount2(dest.c_str(), MNT_DETACH))
    {
      std::cerr << "Unmounting of " << dest <<
                   " failed: " << strerror(errno) << std::endl;
    }

    sfs::remove_all(dest);
  }
}

void startApp(std::string& chrootPath, std::vector<std::string>& deps)
{
  pid_t pid = fork();

  switch (pid)
  {
    case -1:
      /* Couldn't fork */
      {
        std::cerr << "Couldn't fork" << std::endl;
      }
      break;
    case 0:
      /* Child process */
      {
          changeRoot(chrootPath);

          std::string execFile = "./" + deps[0];
          if (execlp(execFile.c_str(), execFile.c_str(), NULL) == -1)
          {
            std::cerr << "Execution of " << deps[0] << " failed: "
                      << strerror(errno) << std::endl;
          }
          // The child is gone now
      }
      break;
    default:
      /* Parent process */
      {
        std::cout << "Parent: I put my child in a sandbox" << std::endl;
        waitpid(pid, NULL, 0);
      }
      break;
  }
}

void changeRoot(std::string& chrootPath)
{
  if (chroot(chrootPath.c_str()) == -1)
  {
    std::cerr << "Chroot to " << chrootPath << " failed: "
              << strerror(errno) << std::endl;
  }

  if (chdir("/") == -1)
  {
    std::cerr << "Chdir to root failed: "
              << strerror(errno) << std::endl;
  }
}
#include <filesystem>
#include <fstream>
#include <iostream>

namespace sfs = std::filesystem;

int main()
{
  std::cout << "Child: Let's check the root:";
  for (auto& entry : sfs::directory_iterator("/"))
  {
    std::cout << " " << entry.path();
  }
  std::cout << std::endl;

  std::cout << "Child: Oh shit, I have to play in a sandbox" << std::endl;

  return 0;
}
$ g++ child.cpp -o child.out

$ ldd child.out
linux-vdso.so.1 (0x00007ffcc8375000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f50bb800000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50bb400000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f50bb715000)
/lib64/ld-linux-x86-64.so.2 (0x00007f50bbb1e000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f50bbae3000)

$ g++ main.cpp -o main.out

$ sudo ./main.out
Parent: I put my child in a sandbox
Child: Let's check the root: "/child.out" "/lib" "/lib64"
Child: Oh shit, I have to play in a sandbox