Files
isolationRunner/Runner.cpp
tomFlowee c0f579ff6d add VPN feature
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.
2026-04-11 15:06:44 +02:00

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;
}
}
}