qswiki/sandbox/sandbox-linux.cpp

273 lines
6.3 KiB
C++

#include <sys/prctl.h>
#include <seccomp.h>
#include <vector>
#include <initializer_list>
#include <string.h>
#include <sched.h>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <filesystem>
#include <sys/mount.h>
#include <sys/capability.h>
#include "../logger.h"
#include "../utils.h"
#include "../random.h"
#include "sandbox-linux.h"
/* TODO: make a whitelist approach. So far we simply blacklist
* obvious systemcalls. To whitelist, we need to analyse our
* dependencies (http library, sqlite wrapper, sqlite lib etc.) */
/* TODO: cleanup our sandboxing directory (unmount and delete folders) after exit */
bool SandboxLinux::seccomp_blacklist(std::initializer_list<int> syscalls)
{
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if(ctx == NULL)
{
Logger::error() << "failed to init seccomp_init";
return false;
}
for(auto sc : syscalls)
{
if(seccomp_rule_add(ctx, SCMP_ACT_KILL_PROCESS, sc, 0) < 0)
{
Logger::error() << "Failed to add a seccomp rule";
return false;
}
}
bool success = seccomp_load(ctx) == 0;
if(!success)
{
Logger::error() << "Failed to load seccomp filter";
return false;
}
return success;
}
bool SandboxLinux::bindMountPaths(std::string target_root, std::initializer_list<std::string> paths)
{
for(const std::string &path : paths)
{
std::string chroot_target_path = target_root + path;
if(std::filesystem::exists(chroot_target_path))
{
continue;
}
if(std::filesystem::is_regular_file(path))
{
std::fstream f1;
f1.open(chroot_target_path, std::ios::out);
f1.close();
}
else {
std::error_code ec;
//TODO: fails if the stuff already exists, but it shouldn't according to doc?
if(!std::filesystem::create_directories(chroot_target_path, ec))
{
Logger::error() << "Error while trying to duplicate structure for sandbox. Dir creation failed. Path: " << chroot_target_path << " Error: " << ec.message();
return false;
}
if(mount(path.c_str(), chroot_target_path.c_str(), NULL, MS_BIND, NULL) == -1)
{
Logger::error() << "Bind mount failed! " << strerror(errno);
return false;
}
}
}
return true;
}
bool SandboxLinux::isolateNamespaces(std::vector<std::string> fsPaths)
{
auto current_uid = getuid();
auto current_gid = getgid();
if(unshare(CLONE_NEWUSER) == -1)
{
Logger::error() << "Failed to unshare user namespace: " << strerror(errno);
return false;
}
std::fstream setgroups;
setgroups.open("/proc/self/setgroups", std::ios::out | std::ios::app);
setgroups << "deny";
setgroups.flush();
setgroups.close();
std::fstream uid_map;
uid_map.open("/proc/self/uid_map", std::ios::out | std::ios::app);
uid_map << "0 " << current_uid << " 1\n";
uid_map.flush();
uid_map.close();
std::fstream gid_map;
gid_map.open("/proc/self/gid_map", std::ios::out);
uid_map << "0 " << current_gid << " 1\n";
gid_map.flush();
gid_map.close();
if(unshare(CLONE_NEWNS) == -1)
{
Logger::error() << "Failed to unshare mount namespace: " << strerror(errno);
return false;
}
/*
* TODO: breaks server.
* TODO: fork?
* if(unshare(CLONE_NEWPID) == -1)
{
Logger::error() << "Failed to unshare pid namespace: " << strerror(errno);
return false;
}*/
//The purpose is to start with a clean sandbox dir.
//We maybe could work with mkdirat, and check whether it exists alrady, to avoid
//some attacks where an attacker gueses the dir, but in that case the system is already compromised
//TODO: still, check, whether this is something we must consider here or not...
Random random;
std::string rootpath = "/tmp/qswiki_sandbox_" + random.getRandomHexString(10) + "/";
if(!std::filesystem::create_directory(rootpath))
{
Logger::error() << "Failed to create chroot directory for sandbox";
return false;
}
for(std::string &path : fsPaths)
{
if(!bindMountPaths(rootpath, { path }))
{
Logger::error() << "Bind mount for " << path << " failed!";
return false;
}
}
if(chroot(rootpath.c_str()) == -1)
{
Logger::error() << "chroot to sandbox failed!";
return false;
}
if(chdir("/") == -1)
{
Logger::error() << "chdir to sandbox failed!";
return false;
}
return true;
}
bool SandboxLinux::enableForInit()
{
umask(0027);
//TODO. there is execv for SPARC. Sigh...
if(!seccomp_blacklist({ SCMP_SYS(execveat), SCMP_SYS(execve) }))
{
Logger::error() << "Failed to install blacklisting seccomp filter";
return false;
}
return true;
}
bool SandboxLinux::enablePreWorker(std::vector<std::string> fsPaths)
{
if(!isolateNamespaces(fsPaths))
{
Logger::error() << "Failed to isolate namespaces";
return false;
}
return true;
}
bool SandboxLinux::supported()
{
return true;
}
bool SandboxLinux::enableForWorker()
{
int cap = 0;
int res = 0;
while((res = prctl(PR_CAPBSET_DROP, cap++, 0, 0, 0)) == 0);
if(res == -1 && errno != EINVAL)
{
Logger::error() << "Failed to drop the capability bounding set!";
return false;
}
__user_cap_header_struct h = { 0 };
h.pid = 0;
h.version = _LINUX_CAPABILITY_VERSION_3;
__user_cap_data_struct drop[2];
drop[0].effective = 0;
drop[0].permitted = 0;
drop[0].inheritable = 0;
drop[1].effective = 0;
drop[1].permitted = 0;
drop[1].inheritable = 0;
if(capset(&h, drop) == -1)
{
Logger::error() << "Failed to drop capabilities: " << strerror(errno);
return false;
}
/* TODO: as said, a whitelist approach is better. As such, this list is bound to be incomplete in the
* sense that more could be listed here and some critical ones are probably missing */
if(! seccomp_blacklist({
SCMP_SYS(setuid),
SCMP_SYS(setuid32),
SCMP_SYS(connect),
SCMP_SYS(chroot),
SCMP_SYS(pivot_root),
SCMP_SYS(mount),
SCMP_SYS(setns),
SCMP_SYS(unshare),
SCMP_SYS(ptrace),
SCMP_SYS(personality)
}))
{
Logger::error() << "Sandbox: Activation of seccomp blacklist failed!";
return false;
}
if(prctl(PR_SET_DUMPABLE, 0) == -1)
{
Logger::error() << "prctl: PR_SET_DUMPABLE failed";
return false;
}
if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1)
{
Logger::error() << "prctl: PR_SET_NO_NEW_PRIVS failed: " << strerror(errno);
return false;
}
if(! seccomp_blacklist({ SCMP_SYS(prctl) }))
{
Logger::error() << "Sandbox: Activation of seccomp blacklist failed!";
return false;
}
return true;
}