You've already forked isolationRunner
c0f579ff6d
This allows a jail to have a VPN config associated and as a result we start a new net namespace, completely isolating the jails networking. We then start an openVPN client to route between the main network and the jails' network. The main limitation here is that we don't setup DNS, which basically means that the VPN will route DNS calls to the other side, but since we don't remount resolv.conf this depends on the vpn provider actually mapping the nameserver we use. For people that use a nameserver like 192.168.100.1, this most of the time works just fine. Improvement is possible.
878 lines
30 KiB
C++
878 lines
30 KiB
C++
#include "Runner.h"
|
|
|
|
#include <cassert>
|
|
#include <regex>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/mount.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include <atomic>
|
|
|
|
#define PIPE_READ 0
|
|
#define PIPE_WRITE 1
|
|
|
|
// the waiting thread.
|
|
void waitForChildren(pid_t pid, const int encFsWatchDogPipe)
|
|
{
|
|
static std::atomic_int childCount(0);
|
|
childCount.fetch_add(1);
|
|
// This small process waits until the child finished
|
|
// the 'wait' also cleans up the kernel process table and afterwards
|
|
// we simply exit.
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
|
|
if (childCount.fetch_sub(1) == 1) {
|
|
const char *homedir = getenv("HOME");
|
|
assert (homedir); // we checked that in the main.cpp
|
|
try {
|
|
std::filesystem::path path(homedir);
|
|
path /= std::string(".iso-pipe");
|
|
std::filesystem::remove(path);
|
|
} catch (const std::exception &e) {
|
|
// realistically, the next line is irrelevant since nobody is listening.
|
|
fprintf(stderr, "failed to remove iso-pipe. Error: %s\n", e.what());
|
|
}
|
|
if (encFsWatchDogPipe)
|
|
write(encFsWatchDogPipe, "go!", 3); // the watchdog sends the kill signal to the encFS process.
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The jailed app may use some shell features that detach and they will end up
|
|
* be reparented to the 'jailer' process.
|
|
* To avoid anyone becoming zombies we simply wait in a loop.
|
|
*/
|
|
void waitForChildren2()
|
|
{
|
|
while (true) {
|
|
wait(nullptr);
|
|
}
|
|
}
|
|
|
|
void waitForEncFs(pid_t pid)
|
|
{
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
// if the watched encfs process dies, typically because of an
|
|
// incorrect password, the starting of the jail should be cancelled.
|
|
exit(0);
|
|
}
|
|
|
|
Runner::Runner(const Message &message, int errorFile)
|
|
: m_outputFD(errorFile),
|
|
m_message(message)
|
|
{
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isJailId()) {
|
|
m_jailId = i.jailId();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Runner::setOwnerUserId(uint32_t uid)
|
|
{
|
|
assert(uid > 0);
|
|
m_ownerUid = uid;
|
|
}
|
|
|
|
void Runner::setProcessName(char *name, int allocatedSize)
|
|
{
|
|
m_processName = name;
|
|
m_processNameSize = allocatedSize;
|
|
}
|
|
|
|
void Runner::run()
|
|
{
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return;
|
|
}
|
|
if (pid) {// we are the parent (pid = child's pid)
|
|
// printf("Fork one done, created %d (I'm %d). going back to listening\n", pid, getpid());
|
|
return;
|
|
}
|
|
// remove SIGCHILD handling we inherited from parent.
|
|
struct sigaction act;
|
|
memset(&act, 0, sizeof(act));
|
|
sigaction(SIGCHLD, &act, 0);
|
|
|
|
// this fork doesn't communicate with the user-space app.
|
|
for (auto fd : m_pipes) {
|
|
close(fd); // cleanup
|
|
}
|
|
|
|
/*
|
|
* While still fully root, start the encfs base, if needed.
|
|
*/
|
|
int encFSWatchDog = 0;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isJailPwd()) {
|
|
// we decrypt the jail with the password given.
|
|
encFSWatchDog = runEncFs(i.stringPtr(), i.stringLength());
|
|
if (encFSWatchDog == 0) {
|
|
fprintf(stderr, "Failed to run encfs\n");
|
|
exit(18);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* VPN support
|
|
*/
|
|
bool hasVpn = false;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isVpnConfig()) {
|
|
m_vpnBasePath = i.stringPtr();
|
|
hasVpn = true;
|
|
}
|
|
else if (i.isVpnPwdBool()) {
|
|
m_vpnHasPwdFile = i.boolData();
|
|
}
|
|
}
|
|
|
|
if (hasVpn) {
|
|
const std::string jailId = std::to_string(m_jailId);
|
|
m_netNsName = "iso-jail" + jailId;
|
|
// Clear and create fresh netns
|
|
system(("/usr/bin/ip netns delete " + m_netNsName + " >/dev/null 2>&1").c_str());
|
|
system(("/usr/bin/ip link delete veth-" + m_netNsName + " >/dev/null 2>&1").c_str());
|
|
system(("/usr/bin/ip netns add " + m_netNsName).c_str());
|
|
// add a loopback
|
|
system(("/usr/bin/ip netns exec " + m_netNsName + " ip link set dev lo up").c_str());
|
|
}
|
|
|
|
/*
|
|
* 'unshare' is the nicest way to run a program in a new namespace.
|
|
* PID 'unshare' means that the to-be-run application can not see which other
|
|
* processes are running outside of its own, app-local, namespace.
|
|
* IPC 'unshare' means that shared memory, pipes etc which running processes
|
|
* may make available are likewise namespaced away from the application.
|
|
* NEWNS 'unshare' means we fork the filesystem, allowing us to do mounts
|
|
* and umounts that change what the child process can see.
|
|
*/
|
|
if (-1 == unshare(CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS)) {
|
|
fprintf(stderr, "Runner: Failed to unshare\n");
|
|
exit(0);
|
|
}
|
|
|
|
signal(SIGINT, SIG_IGN);
|
|
signal(SIGTERM, SIG_IGN);
|
|
// we make a second fork of the process because that also forks the PID namespace.
|
|
// the child is (likely) getting PID 1
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return;
|
|
}
|
|
|
|
if (pid) {// parent
|
|
char messageBuf[20];
|
|
snprintf(messageBuf, sizeof(messageBuf) - 1, "%d", pid);
|
|
sendUpstream(messageBuf);
|
|
exit(0); // Let the jailer do it's own thing from here on out.
|
|
}
|
|
close(m_outputFD);
|
|
|
|
// mounts, first propagation
|
|
if (mount("none", "/", nullptr, MS_REC | MS_PRIVATE, nullptr)) {
|
|
fprintf(stderr, "cannot change root filesystem propagation\n");
|
|
exit(1);
|
|
}
|
|
// get us a proc
|
|
if (mount("proc", "/proc", "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, nullptr)) {
|
|
fprintf(stderr, "Runner: Failed to mount proc\n");
|
|
exit(1);
|
|
}
|
|
|
|
int newEnvCount = 0;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isNewEnvVar())
|
|
++newEnvCount;
|
|
else if (i.isUnmount()) {
|
|
|
|
auto md = i.mountData();
|
|
if (md.src.empty()) {
|
|
fprintf(stderr, "Runner: internal error\n");
|
|
exit(1); // internal error.
|
|
}
|
|
if (umount2(md.src.c_str(), UMOUNT_NOFOLLOW) == -1) {
|
|
if (!i.isTry()) {
|
|
perror("Runner: umount failed");
|
|
fprintf(stderr, " '%s'\n", md.src.c_str());
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
else if (i.isRemount()) {
|
|
auto md = i.mountData();
|
|
if (md.src.empty() || md.dst.empty()) {
|
|
fprintf(stderr, "Runner: internal error\n");
|
|
exit(1); // internal error.
|
|
}
|
|
|
|
struct stat fromDetails;
|
|
if (lstat(md.src.c_str(), &fromDetails)) {
|
|
if (i.isTry()) // source missing
|
|
continue;
|
|
perror("Runner: failed due to rbind from a non-existing source");
|
|
exit(1);
|
|
}
|
|
// TODO verify all parts of the source path are available to USER.
|
|
const bool sourceIsSock = (fromDetails.st_mode & S_IFSOCK) == S_IFSOCK;
|
|
const bool sourceIsDir = !sourceIsSock && (fromDetails.st_mode & S_IFDIR) == S_IFDIR;
|
|
// create dest-dir before mounting on it
|
|
std::filesystem::path dest(md.dst);
|
|
std::filesystem::path destDir(dest);
|
|
bool destIsDir = !dest.has_filename();
|
|
bool destExists = false;
|
|
struct stat toDetails;
|
|
if (lstat(md.dst.c_str(), &toDetails) == 0) {
|
|
destIsDir = (toDetails.st_mode & S_IFDIR) == S_IFDIR;
|
|
destExists = true;
|
|
}
|
|
if (!destIsDir) {
|
|
if (destExists && sourceIsDir) {
|
|
fprintf(stderr, "Runner: rbind failed, source is dir, dest is not\n");
|
|
exit(1);
|
|
}
|
|
if (!sourceIsDir)
|
|
destDir.remove_filename();
|
|
}
|
|
else if (!sourceIsDir) {
|
|
fprintf(stderr, "Runner: rbind failed, dest is dir, source is not");
|
|
exit(1);
|
|
}
|
|
mkdirs(destDir);
|
|
if (!sourceIsDir && !destIsDir && !destExists) {
|
|
// printf(" creating file to mount on %s\n", dest.string().c_str());
|
|
// then we need a simple file there.
|
|
std::ofstream file;
|
|
file.open(dest);
|
|
file.close();
|
|
}
|
|
|
|
unsigned long mountOptions = MS_BIND;
|
|
if (sourceIsDir)
|
|
mountOptions |= MS_REC; // recursive
|
|
if (mount(md.src.c_str(), md.dst.c_str(), nullptr, mountOptions, nullptr)) {
|
|
if (!i.isTry()) {
|
|
perror("Runner: rbind failed");
|
|
fprintf(stderr, " %s -> %s\n", md.src.c_str(), md.dst.c_str());
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
else if (i.isCreateTmp()) {
|
|
auto md = i.mountData();
|
|
if (md.dst.empty()) {
|
|
fprintf(stderr, "Runner: internal error\n");
|
|
exit(1); // internal error.
|
|
}
|
|
if (!i.isTry()) { // if it is a 'try' we won't create the dir first.
|
|
struct stat destDetails;
|
|
if (lstat(md.dst.c_str(), &destDetails)) {
|
|
if (mkdir(md.dst.c_str(), 0777)) {
|
|
perror("Runner: tmpfs's mkdir failed");
|
|
exit(1);
|
|
}
|
|
}
|
|
else if ((destDetails.st_mode & S_IFDIR) != S_IFDIR) { // is not dir
|
|
perror("Runner: failed due to tmpfs over a non-dir");
|
|
exit(1);
|
|
}
|
|
|
|
}
|
|
if (mount("tmpfs", md.dst.c_str(), "tmpfs", MS_NOSUID|MS_NODEV, nullptr)) {
|
|
if (!i.isTry()) {
|
|
fprintf(stderr, "Runner: Failed to create tmpfs at %s\n", md.dst.c_str());
|
|
exit(1);
|
|
}
|
|
}
|
|
chown(md.dst.c_str(), m_ownerUid, getgid());
|
|
}
|
|
else if (i.isCopy()) {
|
|
auto cm = i.copyData();
|
|
if (cm.from.empty() || cm.to.empty()
|
|
|| cm.from.at(0) != '/' || cm.to.at(0) != '/') {
|
|
fprintf(stderr, "Runner: invalid copy request.\n");
|
|
exit(1);
|
|
}
|
|
// is destination a dir?
|
|
const std::filesystem::path to(cm.to);
|
|
if (!to.has_filename()) { // copy to dir requested
|
|
if (!i.isTry())
|
|
mkdirs(to);
|
|
struct stat destDetails;
|
|
if (lstat(cm.to.c_str(), &destDetails) == 0) {
|
|
if ((destDetails.st_mode & S_IFDIR) != S_IFDIR) {
|
|
if (i.isTry()) // lets not even call 'copy'
|
|
continue;
|
|
fprintf(stderr, "Runner: copy's target expected dir, but is not\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
if (!runCopy(cm.from, cm.to)) {
|
|
if (!i.isTry()) {
|
|
fprintf(stderr, "Runner: Failed to copy %s\n", cm.from.c_str());
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
else if (i.isDBusMapping()) {
|
|
auto map = i.dbusMapping();
|
|
// a dbus mapping is provided by the default tool '/bin/xdg-dbus-proxy'
|
|
// we start one now.
|
|
|
|
if (map.systemBus) // ignore this one for now, not sure what to filter yet.
|
|
continue;
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return;
|
|
}
|
|
if (pid == 0) {// we are the child
|
|
if (setuid(m_ownerUid) != 0 || seteuid(m_ownerUid) != 0) {
|
|
exit(1);
|
|
}
|
|
char *arguments[9];
|
|
arguments[0] = const_cast<char*>("/bin/xdg-dbus-proxy");
|
|
arguments[1] = const_cast<char*>(map.from.c_str());
|
|
arguments[2] = const_cast<char*>(map.to.c_str());
|
|
arguments[3] = const_cast<char*>("--filter");
|
|
arguments[4] = const_cast<char*>("--call=org.freedesktop.Notifications=freedesktop/Notifications.*@org");
|
|
arguments[5] = const_cast<char*>("--call=org.freedesktop.Notifications=kde/Notifications.*@org");
|
|
arguments[6] = const_cast<char*>("--call=org.freedesktop.portal.Desktop=*@/org/freedesktop/portal/desktop/*");
|
|
arguments[7] = const_cast<char*>("--own=org.kde.*");
|
|
arguments[8] = 0;
|
|
execv(arguments[0], arguments);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare for normal users
|
|
const char *homedir = getenv("HOME");
|
|
assert (homedir); // we checked that in the main.cpp
|
|
if (chdir(homedir)) {
|
|
fprintf(stderr, "Runner: Failed to change to dir\n");
|
|
exit(1);
|
|
}
|
|
if (!m_netNsName.empty()) { // Start openVPN process
|
|
/*
|
|
* We start OpenVPN in the jail-ed namespaces for process-ip and the full mount setup.
|
|
* But openVPN will run in the global networking namespace.
|
|
* After it has made a connection we'll create the netns and move it there
|
|
* and indeed force the jail to use the new netns too.
|
|
*/
|
|
assert (!m_vpnBasePath.empty());
|
|
|
|
auto ovpn = m_vpnBasePath + ".ovpn";
|
|
auto sh = m_vpnBasePath + ".sh";
|
|
auto ac = m_vpnBasePath + "ac";
|
|
// the users process copied the config files to the target jail and
|
|
// gave them protection mask 0400.
|
|
// It's not really needed, but why not make at least the password
|
|
// file root jailed process can see them, but can't read them.
|
|
if (!m_vpnHasPwdFile && chown(ac.c_str(), 0, 0)) {
|
|
fprintf(stderr, "Runner: Failed to protect vpn ac\n");
|
|
exit(0);
|
|
}
|
|
|
|
auto pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
exit(0);
|
|
}
|
|
else if (pid == 0) { // child
|
|
char *arguments[] = {
|
|
const_cast<char*>("/usr/sbin/openvpn"),
|
|
const_cast<char*>("--config"), const_cast<char*>(ovpn.c_str()),
|
|
const_cast<char*>("--auth-user-pass"), const_cast<char*>(ac.c_str()),
|
|
// needed to run our scripts
|
|
const_cast<char*>("--script-security"), const_cast<char*>("2"),
|
|
|
|
// notice that /tmp is personal to this jail
|
|
const_cast<char*>("--log"), const_cast<char*>("/tmp/openvpn.log"),
|
|
const_cast<char*>("--up"), const_cast<char*>(sh.c_str()),
|
|
const_cast<char*>("--down"), const_cast<char*>(sh.c_str()),
|
|
nullptr
|
|
};
|
|
// continue life as openvpn
|
|
execv(arguments[0], arguments);
|
|
}
|
|
assert(pid > 0);
|
|
|
|
// poll for the generated script
|
|
for (int i = 0; i < 60; ++i) {
|
|
struct stat conf;
|
|
if (lstat("/tmp/vpnconf.sh", &conf) == 0)
|
|
break;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
}
|
|
system("/tmp/vpnconf.sh"); // configure the network stack
|
|
|
|
std::string nsPath = "/var/run/netns/" + m_netNsName;
|
|
int nsFd = open(nsPath.c_str(), O_RDONLY);
|
|
if (nsFd == -1) {
|
|
perror("Runner: open netns");
|
|
exit(1);
|
|
}
|
|
if (setns(nsFd, CLONE_NEWNET) == -1) {
|
|
perror("Runner: setns");
|
|
exit(1);
|
|
}
|
|
close(nsFd);
|
|
}
|
|
// drop permissions
|
|
if (setuid(m_ownerUid) != 0 || seteuid(m_ownerUid) != 0) {
|
|
fprintf(stderr, "Runner: Failed to change UID\n");
|
|
exit(1);
|
|
}
|
|
|
|
// check if there is an init script and run that as a child process.
|
|
if (runInitScript()) {
|
|
fprintf(stderr, "Runner: init script failed\n");
|
|
exit(1);
|
|
}
|
|
|
|
// no point keeping those open.
|
|
fclose(stdin);
|
|
fclose(stdout);
|
|
fclose(stderr);
|
|
|
|
// Keep this process waiting for more requests to start in this 'jail'.
|
|
// the real process is going to be the child.
|
|
pid = fork();
|
|
if (pid == -1) exit(1);
|
|
if (pid) { // parent
|
|
// printf("Fork 3 done, created %d (I'm %d). waiting for child to exit\n", pid, getpid());
|
|
renameThisProcess(m_processName, m_processNameSize, "jailer");
|
|
new std::thread(waitForChildren, pid, encFSWatchDog);
|
|
new std::thread(waitForChildren2);
|
|
|
|
// create the pipe that indicates this jail is occupied.
|
|
// the pipe can be used by the dispatcher to send us more things to run
|
|
// in this jail.
|
|
mkfifo(".iso-pipe", 0750);
|
|
|
|
/*
|
|
* Wait for commands from the dispatcher. We may be asked to run another
|
|
* app in the existing jail.
|
|
*
|
|
* This is a pipe, we open it for read. Read all there is and other side
|
|
* will close the pipe when they pushed through their message.
|
|
* We'll open it again for the next message.
|
|
*/
|
|
char buf[Message::MAX_SIZE + 1];
|
|
int msgSize = 0;
|
|
while (true) {
|
|
std::ifstream controlChannel;
|
|
controlChannel.open(".iso-pipe", std::ios_base::in);
|
|
if (!controlChannel.is_open()) {
|
|
exit(0);
|
|
}
|
|
while (controlChannel.is_open()) {
|
|
controlChannel.read(buf + msgSize, sizeof(buf) - msgSize);
|
|
auto read = controlChannel.gcount();
|
|
if (read == 0) // pipe is closed, lets process
|
|
break;
|
|
msgSize += read;
|
|
if (msgSize >= (int) sizeof(buf)) {// message too big. Ignore.
|
|
msgSize = 0;
|
|
break;
|
|
}
|
|
}
|
|
if (msgSize) {
|
|
pid = fork();
|
|
if (pid == -1) exit(1);
|
|
if (pid) { // I'm parent
|
|
// pid is child's pid, lets wait for them in a thread.
|
|
new std::thread(waitForChildren, pid, encFSWatchDog);
|
|
}
|
|
else { // run up to exec, below!
|
|
m_message = Message(buf, msgSize);
|
|
newEnvCount = 0;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isNewEnvVar())
|
|
++newEnvCount;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* GUI applications want to run much better when they are started by bash,
|
|
* so why not?
|
|
*/
|
|
char *arguments[4];
|
|
arguments[0] = const_cast<char*>("/usr/bin/bash");
|
|
arguments[1] = const_cast<char*>("-c");
|
|
std::string args(m_message.path());
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isArgument())
|
|
args += std::string(" '") + i.argument() + "'";
|
|
}
|
|
arguments[2] = const_cast<char*>(args.c_str());
|
|
arguments[3] = 0;
|
|
|
|
int count = 0;
|
|
while (environ[count])
|
|
++count;
|
|
count += newEnvCount;
|
|
|
|
auto envList = new char*[count + 1]; // yap, that's a memleak. :shrug:
|
|
memset(envList, 0, (count + 1) * sizeof(char*));
|
|
copyFilteredEnv(environ, envList);
|
|
assert(envList[count] == 0);
|
|
|
|
execve(arguments[0], arguments, envList);
|
|
exit(1);
|
|
}
|
|
|
|
void Runner::sendUpstream(const char *errorMessage)
|
|
{
|
|
char messageBuf[256];
|
|
int size = snprintf(messageBuf, sizeof(messageBuf) - 1,
|
|
"%u %s", m_jailId, errorMessage);
|
|
messageBuf[size] = '\0';
|
|
// notice that on Linux single pipe writes (up to PIPE_BUF, some 4K) are atomic
|
|
// which protects us from multiple clients messages interacting.
|
|
write(m_outputFD, messageBuf, size + 1);
|
|
}
|
|
|
|
bool Runner::runCopy(const std::string &from, const std::filesystem::path &to) const
|
|
{
|
|
// the 'from' may include a '*' wildcard, which makes all things a little harder...
|
|
auto index = from.find('*');
|
|
if (index == std::string::npos) { // straight-forward copy
|
|
std::filesystem::path fromFilePath(from);
|
|
auto fullTo = to;
|
|
if (!fullTo.has_filename()) // if the target is a directory, append the src filename
|
|
fullTo /= fromFilePath.filename();
|
|
return copySingle(fromFilePath, fullTo);
|
|
}
|
|
auto slashIndex = from.rfind(std::string("/"));
|
|
if (slashIndex > index) {
|
|
// TODO error out
|
|
return false;
|
|
}
|
|
assert(index != std::string::npos); // caller should have checked
|
|
std::filesystem::path path = from.substr(0, slashIndex);
|
|
std::string pattern = from.substr(slashIndex + 1);
|
|
pattern = pattern.replace(index - slashIndex, 1, ".*");
|
|
|
|
std::regex regEx(std::string("/") + pattern + "$");
|
|
for (const auto &entry : std::filesystem::directory_iterator(path)) {
|
|
if (std::regex_search(entry.path().string(), regEx)) {
|
|
std::filesystem::path source(entry.path());
|
|
bool ok = copySingle(source, to / source.filename());
|
|
if (!ok)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Runner::copySingle(const std::filesystem::path &from, const std::filesystem::path &to) const
|
|
{
|
|
// std::cout << from.string() << " -> " << to.string() << std::endl;
|
|
// TODO we copy as root, so this is dangerous!
|
|
// FIXME check permissions of each of the path elements for the target user.
|
|
|
|
try {
|
|
if (!std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing))
|
|
return false;
|
|
} catch (const std::exception &e) {
|
|
// std::cout << "Copy instruction failed with: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
// stat and apply owner and permissions.
|
|
std::string fromStr(from.string());
|
|
struct stat source;
|
|
if (lstat(fromStr.c_str(), &source)) {
|
|
assert(false);
|
|
return false;
|
|
}
|
|
// st mode
|
|
std::string toStr(to.string());
|
|
chmod(toStr.c_str(), source.st_mode);
|
|
chown(toStr.c_str(), source.st_uid, source.st_gid);
|
|
return true;
|
|
}
|
|
|
|
void Runner::copyFilteredEnv(char **from, char **target)
|
|
{
|
|
while (*from) {
|
|
auto equals = strchrnul(*from, '=');
|
|
const int len = equals - *from;
|
|
if (len <= 0)
|
|
continue;
|
|
bool filtered = false;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if ((i.isNewEnvVar() || i.isEnvVarUnset())
|
|
&& i.stringLength() >= len
|
|
&& strncmp(i.stringPtr(), *from, len) == 0) {
|
|
filtered = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!filtered) {
|
|
*target = *from;
|
|
++target;
|
|
}
|
|
++from;
|
|
}
|
|
|
|
// finally, append the 'set' properties
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isNewEnvVar()) {
|
|
*target = i.stringPtr();
|
|
++target;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Runner::mkdirs(const std::filesystem::path &dir) const
|
|
{
|
|
std::filesystem::path partial;
|
|
bool userIdOk = false;
|
|
bool permissionsOk = false;
|
|
for (auto chunk = dir.begin(); chunk != dir.end(); ++chunk) {
|
|
if (partial.empty()) {
|
|
partial = *chunk;
|
|
userIdOk = false;
|
|
} else {
|
|
partial /= *chunk;
|
|
struct stat dirStat;
|
|
if (lstat(partial.string().c_str(), &dirStat) == 0) {
|
|
userIdOk = dirStat.st_uid == m_ownerUid;
|
|
permissionsOk = (dirStat.st_mode & 3) == 3;
|
|
if (!userIdOk && !permissionsOk && ((dirStat.st_mode & 0x30) == 0x30)) {
|
|
// it is readable to the group, lets check if that includes m_ownerId
|
|
// ok, we could iterate over all groups, but I'm lazy and we don't need
|
|
// that today, so this is simpler.
|
|
permissionsOk = dirStat.st_gid == getgid();
|
|
}
|
|
}
|
|
else { // does not exist, should we create it?
|
|
const std::string path = partial.string();
|
|
if (!userIdOk && !permissionsOk) {
|
|
fprintf(stderr, "Runner: mkdir in another user. Refusing [%s]\n", path.c_str());
|
|
exit(1);
|
|
}
|
|
|
|
if (mkdir(path.c_str(), 0700)) {
|
|
perror("Runner: target-dir mkdir() failed");
|
|
fprintf(stderr, " '%s'\n", path.c_str());
|
|
exit(1);
|
|
}
|
|
chown(path.c_str(), m_ownerUid, getgid());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int Runner::runInitScript()
|
|
{
|
|
int length = 0;
|
|
const char *script = nullptr;
|
|
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
|
|
if (i.isInitSript()) {
|
|
length = i.stringLength();
|
|
script = i.stringPtr();
|
|
break;
|
|
}
|
|
}
|
|
if (length == 0)
|
|
return 0;
|
|
|
|
std::ofstream scriptFile;
|
|
constexpr const char *TmpFileName = "/tmp/iso-initscript.sh";
|
|
scriptFile.open(TmpFileName);
|
|
if (!scriptFile.is_open()) {
|
|
fprintf(stderr, "Failed to create init script in /tmp");
|
|
return 1;
|
|
}
|
|
scriptFile.write(script, length);
|
|
scriptFile.flush();
|
|
scriptFile.close();
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return 1; // if this one failed, the next on by calling method will too.
|
|
}
|
|
if (pid) {// we are the parent (pid = child's pid)
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
return status;
|
|
}
|
|
|
|
char *arguments[3];
|
|
arguments[0] = const_cast<char*>("/usr/bin/bash");
|
|
arguments[1] = const_cast<char*>(TmpFileName);
|
|
arguments[2] = 0;
|
|
execv(arguments[0], arguments);
|
|
return 0;
|
|
}
|
|
|
|
int Runner::runEncFs(const char *password, int strlen) const
|
|
{
|
|
int aStdinPipe[2];
|
|
if (pipe(aStdinPipe) < 0) {
|
|
perror("allocating pipe for encfs");
|
|
return 0;
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return 0;
|
|
}
|
|
char buf[20];
|
|
snprintf(buf, sizeof(buf), "%d", m_jailId);
|
|
const std::string jailName(buf);
|
|
char path[2000];
|
|
char *curDir = getcwd(path, sizeof(path));
|
|
if (curDir == nullptr) {
|
|
fprintf(stderr, "Runner: runEncFs: path too long\n");
|
|
return 0;
|
|
}
|
|
const std::string curDirStr(curDir);
|
|
std::string jaildir = curDirStr + "/" + jailName;
|
|
|
|
if (pid == 0) { // we are the child
|
|
// redirect stdin to our pipe
|
|
if (dup2(aStdinPipe[PIPE_READ], STDIN_FILENO) == -1) {
|
|
exit(errno);
|
|
}
|
|
|
|
// these are for use by parent only
|
|
close(aStdinPipe[PIPE_READ]);
|
|
close(aStdinPipe[PIPE_WRITE]);
|
|
// close(aStdoutPipe[PIPE_READ]);
|
|
// close(aStdoutPipe[PIPE_WRITE]);
|
|
close(STDOUT_FILENO);
|
|
close(STDERR_FILENO);
|
|
|
|
char *arguments[7];
|
|
arguments[0] = const_cast<char*>("/usr/bin/encfs");
|
|
arguments[1] = const_cast<char*>("-f"); // foreground. Don't fork.
|
|
arguments[2] = const_cast<char*>("--public");
|
|
arguments[3] = const_cast<char*>("--stdinpass");
|
|
std::string backend = curDirStr + "/." + jailName;
|
|
arguments[4] = const_cast<char*>(backend.c_str());
|
|
arguments[5] = const_cast<char*>(jaildir.c_str());
|
|
arguments[6] = 0;
|
|
|
|
execv(arguments[0], arguments);
|
|
}
|
|
else {// we are the parent (pid = child's pid)
|
|
close(aStdinPipe[PIPE_READ]);
|
|
|
|
// monitor the process, if it dies we should die too.
|
|
new std::thread(waitForEncFs, pid);
|
|
|
|
bool ok = false;
|
|
// next we wait for the target dir to actually become a mount.
|
|
dev_t oldDevice = 0;
|
|
for (int i = 0; i < 200; ++i) { // max 15 sec
|
|
struct stat targetDir;
|
|
if (stat(jaildir.c_str(), &targetDir)) {
|
|
printf("stat of encrypted dir failed '%s'\n", jaildir.c_str());
|
|
exit(1);
|
|
}
|
|
|
|
if (i == 0) {
|
|
// On first loop, send the password to be 100%
|
|
// certain that the mount hasn't done anything yet.
|
|
// then remember the device this dir is on and if that
|
|
// changes then we know that the mount has succeeded.
|
|
oldDevice = targetDir.st_dev;
|
|
write(aStdinPipe[PIPE_WRITE], password, strlen);
|
|
write(aStdinPipe[PIPE_WRITE], "\n", 1);
|
|
}
|
|
else if (oldDevice != targetDir.st_dev) {
|
|
ok = true;
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(75));
|
|
}
|
|
|
|
close(aStdinPipe[PIPE_WRITE]);
|
|
if (!ok) {
|
|
printf("EncFS never did its thing. Wrong password?\n");
|
|
exit(2);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We need to kill it later.
|
|
* Which means we need to have another process that actually has the rights
|
|
* to kill it we can talk to.
|
|
* So, a pipe and a simple process that will 'kill' the encfs process later,
|
|
* on command, it is!
|
|
*/
|
|
|
|
const auto encFsPid = pid;
|
|
int encFsWatchdogPipe[2];
|
|
if (pipe(encFsWatchdogPipe) < 0) {
|
|
perror("allocating pipe for encfs");
|
|
kill(encFsPid, SIGTERM);
|
|
return 0;
|
|
}
|
|
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
fprintf(stderr, "Runner: Failed to fork\n");
|
|
return 0;
|
|
}
|
|
else if (pid == 0) {
|
|
close(encFsWatchdogPipe[PIPE_WRITE]);
|
|
renameThisProcess(m_processName, m_processNameSize, "watchdog-encfs");
|
|
|
|
char buf[10];
|
|
do {
|
|
auto size = read(encFsWatchdogPipe[PIPE_READ], &buf, sizeof(buf));
|
|
if (size < 0)
|
|
exit(0);
|
|
if (size >= 2) {
|
|
kill(encFsPid, SIGTERM);
|
|
exit(0);
|
|
}
|
|
} while(true);
|
|
}
|
|
close(encFsWatchdogPipe[PIPE_READ]);
|
|
|
|
return encFsWatchdogPipe[PIPE_WRITE];
|
|
}
|
|
|
|
void renameThisProcess(char *nameBlob, int blobSize, const char *newName)
|
|
{
|
|
// Maybe more correct is to use prctl(PR_SET_NAME) ?
|
|
assert(nameBlob);
|
|
const auto newLength = strlen(newName);
|
|
if (blobSize > (std::int64_t) newLength) {
|
|
memcpy(nameBlob, newName, newLength);
|
|
for (int i = newLength; i < blobSize; ++i) {
|
|
nameBlob[i] = 0;
|
|
}
|
|
}
|
|
}
|