Files

691 lines
24 KiB
C++
Raw Permalink Normal View History

2021-05-20 12:43:04 +02:00
#include "Message.h"
#include "IsolationManager.h"
2021-05-20 12:43:04 +02:00
2021-05-20 19:08:42 +02:00
#include <unistd.h>
#include <sys/stat.h> // for umask
#include <QDebug>
#include <QDir>
#include <QDirIterator>
2021-05-20 12:43:04 +02:00
#include <QFileInfo>
2021-05-20 19:08:42 +02:00
#include <QSettings>
#include <QStringBuilder>
2021-05-25 14:24:44 +02:00
#include <QStandardPaths>
2024-02-25 23:29:33 +01:00
#include <QTimer>
2024-05-17 11:39:44 +02:00
#include <QInputDialog>
2026-04-11 14:54:32 +02:00
#include <QCryptographicHash>
#include <QBuffer>
namespace {
QByteArray computeFileHash(QFile &file)
{
QCryptographicHash hasher(QCryptographicHash::Md5);
constexpr qint64 CHUNK_SIZE = 64 * 1024;
while (!file.atEnd()) {
const QByteArray chunk = file.read(CHUNK_SIZE);
if (chunk.isEmpty())
break;
hasher.addData(chunk);
}
return hasher.result();
};
bool copyIfDifferent(const QString &source, const QString &target)
{
QFile in(source);
if (!in.open(QIODevice::ReadOnly))
return false;
QFile out(target);
if (out.open(QIODevice::ReadOnly)) {
const QByteArray sourceHash = computeFileHash(in);
assert(!sourceHash.isEmpty()); // crypto failure..
const QByteArray targetHash = computeFileHash(out);
if (!sourceHash.isEmpty() && sourceHash == targetHash)
return true;
}
in.close();
QFile::remove(target);
bool ok = QFile::copy(source, target);
if (ok)
out.setPermissions(QFileDevice::ReadOwner);
out.close();
return ok;
}
}
2021-05-20 19:08:42 +02:00
2024-02-15 23:39:04 +01:00
class RulesError : public std::runtime_error
{
public:
RulesError(const char *message, const QString &filename_ = QString())
: std::runtime_error(message),
filename(filename_)
{
}
const QString filename;
};
2021-05-20 19:08:42 +02:00
IsolationManager::IsolationManager(int inputId, int outputId)
2021-05-20 12:43:04 +02:00
: m_runner(inputId, outputId),
2021-05-25 14:24:44 +02:00
m_listener(this),
2024-02-20 11:39:16 +01:00
m_rulesDir("/etc/security/iso-rules/"),
2021-05-25 14:24:44 +02:00
m_dbdir("1/db/")
2021-05-20 12:43:04 +02:00
{
setObjectName(QLatin1String("IsolationManager")); // For the DBus RPC
2021-05-25 14:24:44 +02:00
2024-05-20 22:02:28 +02:00
m_basedir = QDir::currentPath() % "/";
2021-05-25 14:24:44 +02:00
umask(077); // All we create will only be readable by the owner.
2021-05-20 12:43:04 +02:00
2021-05-20 19:08:42 +02:00
QDir basedir(m_basedir);
2021-05-25 14:24:44 +02:00
// Qt bases this on $HOME, so lets make sure we are not being played here.
2021-05-20 19:08:42 +02:00
if (!basedir.isAbsolute())
throw std::runtime_error("Config error: datadir has to be absolute");
2021-05-20 12:43:04 +02:00
2024-05-20 22:02:28 +02:00
QDirIterator jailChecker(".");
while (jailChecker.hasNext()) {
auto entry = jailChecker.next().mid(2); // snip off the "./"
2021-05-20 19:08:42 +02:00
bool ok;
2024-02-19 19:52:24 +01:00
int jid = entry.toInt(&ok);
if (ok && jid >= m_nextJailId)
m_nextJailId = jid + 1;
2021-05-21 11:41:52 +02:00
}
2024-02-25 19:22:08 +01:00
connect (&m_runner, SIGNAL(receivedMessage(QByteArray)),
this, SLOT(receivedMessageFromRunner(QByteArray)));
2021-05-20 12:43:04 +02:00
}
2024-02-19 19:52:24 +01:00
QString IsolationManager::startApplicationRequest(AppEntry &dbEntry, const QStringList &arguments)
2021-05-20 12:43:04 +02:00
{
2021-08-18 12:22:00 +02:00
assert(dbEntry.appId > 1);
2021-05-20 12:43:04 +02:00
2021-05-20 19:08:42 +02:00
/*
2024-02-15 23:39:04 +01:00
* Each app gets its own subdir under m_basedir using its appid as name.
2021-05-20 19:08:42 +02:00
*/
QDir base(m_basedir);
QString homedir = QString::number(dbEntry.appId);
if (!base.mkpath(homedir))
return QString("Internal error: failed to create environment");
2024-05-17 11:39:44 +02:00
if (dbEntry.jailPassword.isEmpty()
&& QFile::exists(m_basedir % "/." % homedir % "/.encfs6.xml")) {
auto *da = new DelayedApp(dbEntry, arguments, this);
da->askPassword();
return QString();
}
2021-05-20 19:08:42 +02:00
// then ask the priviledged task to take it from here.
2021-08-14 22:00:42 +02:00
Message message(Message::MAX_SIZE);
2024-02-21 11:21:37 +01:00
message.setJailId(dbEntry.appId);
2021-05-21 15:28:57 +02:00
try {
2024-05-17 11:39:44 +02:00
if (!dbEntry.jailPassword.isEmpty())
message.setJailPassword(dbEntry.jailPassword.toStdString());
2024-03-06 11:52:54 +01:00
QString exe = dbEntry.pathToExe;
// resolve symlinks and actually find the real executable.
for (int i = 0; i < 10; ++i) { // avoid endless loops
QFileInfo info(exe);
if (info.isSymbolicLink()) {
exe = info.symLinkTarget();
} else {
break;
}
}
// The user should be able to start executables that are stored in the
// homedir, except that the path to that is going to be different in
// his little jail. Lets special case that...
if (exe.startsWith(QDir::homePath())) {
QString newPath = QDir::homePath();
const auto length = newPath.size();
exe = newPath + QString("/shared%1").arg(exe.mid(length));
}
message.setPath(exe.toStdString());
2024-02-16 16:54:09 +01:00
for (const auto &s : arguments) {
2021-05-21 15:28:57 +02:00
message.addArgument(s.toUtf8().constData());
}
2024-02-15 23:39:04 +01:00
2026-04-11 14:54:32 +02:00
// VPN crap
if (!dbEntry.vpnConf.isEmpty()) {
const QString vpnHomeBase = expandVars(dbEntry, QString("$APPHOME/config/vpn"));
QDir(vpnHomeBase).mkpath(".");
const QString appHomeDir = vpnHomeBase + "/.vpn";
if (!copyIfDifferent(dbEntry.vpnConf, appHomeDir + ".ovpn"))
return "vpn config file not found";
message.setVpnConfig(expandVars(dbEntry, QString("$HOME/config/vpn/.vpn")).toStdString());
if (!dbEntry.vpnAc.isEmpty()) {
message.setHasVpnPassFile(true);
QFileInfo orig(dbEntry.vpnAc);
if (!orig.exists())
return "vpn access controls config file not found";
QFileInfo copy(appHomeDir + "ac");
if (!copy.exists() || orig.lastModified() > copy.lastModified()) {
// we still call copyIfDifferent because lazyness. But as its made only
// readable by root, when we get here, we will do the copy.
if (!copyIfDifferent(dbEntry.vpnAc, appHomeDir + "ac"))
return "vpn AC file can't be copied";
}
}
QBuffer script;
script.open(QIODevice::WriteOnly);
const auto netNsName = QString("iso-jail%1").arg(dbEntry.appId);
script.write("#!/bin/sh\n"
"# OpenVPN passes: $1 = dev, $2 = mtu, $4 = local IP, etc.\n\n");
script.write("myNetNsName=\"");
script.write(netNsName.toLatin1());
script.write("\"\n");
script.write("case \"$script_type\" in\n"
" up)\n"
" if ip netns exec $myNetNsName ip link show $1 >/dev/null 2>&1\n"
" then\n"
// for when it comes up again after being down
" ip netns exec $myNetNsName ip link up $1\n"
" else\n"
" dev=$1; mtu=$2\n"
/* The script is called by openvpn on 'up', which seems to be very much not when it's up.
* So in the script we create a new script that copies all the passed in parameters
* but starts with a while loop checking if the device is up yet. Which may take some seconds.
* Only when it's up do we then we run our networking code to move to the namespace, and
* configure the new interface.
*/
" echo \"i=0; while [ \\$i -lt 10 ]; do ip link show $dev >/dev/null 2>&1"
" && { break; }"
" || { i=\\$((i+1)); sleep 0.5; }; done; [ \\$i -eq 10 ] && exit\" >>/tmp/vpnconf.sh\n"
" echo ip link set dev $dev up netns $myNetNsName mtu $mtu >> /tmp/vpnconf.sh\n"
" echo ip netns exec $myNetNsName ip addr add dev $dev \"$4/${ifconfig_netmask:-30}\" "
"${ifconfig_broadcast:+broadcast \"$ifconfig_broadcast\"} >> /tmp/vpnconf.sh\n"
" echo ip netns exec $myNetNsName ip route add default via \"$route_vpn_gateway\" "
"dev $dev >> /tmp/vpnconf.sh\n"
" chmod 700 /tmp/vpnconf.sh\n"
" fi\n"
" ;;\n"
" down)\n"
" ip netns exec $myNetNsName ip link set dev \"$dev\" down 2>/dev/null || true\n"
" ;;\n"
"esac\n");
script.close();
QFile target(appHomeDir + ".sh");
bool same = false;
const auto &scriptBytes = script.buffer();
if (target.open(QIODevice::ReadOnly)) {
QCryptographicHash hasher(QCryptographicHash::Md5);
hasher.addData(scriptBytes);
const QByteArray sourceHash = hasher.result();
assert(!sourceHash.isEmpty()); // crypto failure..
const QByteArray targetHash = computeFileHash(target);
same = targetHash == sourceHash;
target.close();
}
if (!same) {
QFile::remove(appHomeDir + ".sh");
bool ok = target.open(QIODevice::WriteOnly);
assert(ok);
target.write(scriptBytes);
target.close();
target.setPermissions(QFileDevice::ReadOwner | QFileDevice::ExeOwner);
}
}
2024-02-19 10:47:36 +01:00
applyRules(dbEntry, message, m_rulesDir + "global.rules");
2024-02-20 19:14:25 +01:00
QFileInfo proxy("/bin/xdg-dbus-proxy");
if (proxy.isFile() && proxy.isExecutable()) {
if (dbEntry.isAllowed("dbus")) {
QString target = expandVars(dbEntry, QLatin1String("/run/user/$USERID/bus"));
message.addDBusProxy(Message::UserSessionBus, "unix:path=/run/dbus/user-global",
target.toStdString());
}
if (dbEntry.isAllowed("dbus-system"))
message.addDBusProxy(Message::SystemBus, "unix:path=/run/dbus/system-global",
"/run/dbus/system_bus_socket");
}
else {
qWarning() << "Missing tool 'xdg-dbus-proxy', please install to provide dbus to jailed apps";
}
2024-02-24 11:40:42 +01:00
if (!dbEntry.initScript.isEmpty())
message.addInitSript(dbEntry.initScript.toStdString());
2024-02-15 23:39:04 +01:00
} catch (const RulesError &e) {
if (e.filename.isEmpty())
return QString(e.what());
2024-02-19 19:52:24 +01:00
return QString("%1 '%2'").arg(e.what(), e.filename);
2021-05-21 15:28:57 +02:00
} catch (const std::exception &e) {
return QString("Limits reached");
}
2024-02-25 19:22:08 +01:00
const QString stateInfo = m_basedir % "/1/state-" % QString::number(dbEntry.appId) % ".info";
QSettings runInfo(stateInfo, QSettings::IniFormat);
runInfo.setValue("start", QDateTime::currentDateTimeUtc());
runInfo.remove("error"); // in case we are re-using an old state file.
2024-02-25 23:29:33 +01:00
if (dbEntry.autoDelete) {
runInfo.setValue("autodelete", true);
new AutoDeleter(dbEntry, this);
}
2021-05-20 12:43:04 +02:00
m_runner.runRemote(message);
2024-02-22 16:26:53 +01:00
// Because it is useful, lets point from the profile name to the jail directory
// (a boring int), if it doesn't exist yet.
2024-02-25 23:29:33 +01:00
if (!dbEntry.autoDelete) {
2024-03-06 11:48:16 +01:00
QFileInfo symlink(m_basedir + dbEntry.profileName);
2024-02-25 23:29:33 +01:00
if (!symlink.isSymLink() && !symlink.exists()) {
QFile jail(m_basedir + homedir);
assert(jail.exists());
2024-03-06 11:48:16 +01:00
jail.link(dbEntry.profileName);
2024-02-25 23:29:33 +01:00
}
2024-02-22 16:26:53 +01:00
}
2021-05-20 12:43:04 +02:00
return QString("ok");
}
2021-05-20 19:08:42 +02:00
2024-03-06 11:48:16 +01:00
IsolationManager::AppEntry IsolationManager::lookupApp(const QString &profileName, LookupBehavior behavior)
2021-05-20 19:08:42 +02:00
{
2024-02-25 19:48:41 +01:00
IsolationManager::AppEntry rc;
2024-03-06 11:48:16 +01:00
auto entry = startEditApp(profileName, behavior);
rc.profileName = entry->property("profileName").toString();
rc.appId = entry->value("app-id", -1).toInt();
if (rc.appId == -1) {
assert(behavior == OnlyExisting);
return rc;
}
auto denied = entry->value("denied").toString();
rc.denied = denied.split(' ', Qt::SkipEmptyParts);
auto allowed = entry->value("allowed").toString();
rc.allowed = allowed.split(' ', Qt::SkipEmptyParts);
rc.pathToExe = entry->value("path-to-exe", rc.profileName).toString();
rc.initScript = entry->value("init-script", QString()).toString();
2026-04-11 14:54:32 +02:00
rc.vpnAc = entry->value("vpnAc", QString()).toString();
rc.vpnConf = entry->value("vpnConf", QString()).toString();
2024-03-06 11:48:16 +01:00
return rc;
}
std::unique_ptr<QSettings> IsolationManager::startEditApp(const QString &profileName, LookupBehavior behavior)
{
2024-02-19 19:52:24 +01:00
// this removes some stuff, it makes 'firefox' and '/bin/firefox' point to the same database entry.
2024-03-06 11:48:16 +01:00
QString shortName(profileName);
2024-02-19 19:52:24 +01:00
if (shortName.startsWith("/bin/"))
shortName = shortName.mid(5);
else if (shortName.startsWith("/usr/bin/"))
shortName = shortName.mid(9);
else if (shortName.startsWith("/"))
shortName = shortName.mid(1);
shortName.replace('%', "%25");
shortName.replace('/', "%2f");
2024-03-06 11:48:16 +01:00
auto entry = std::make_unique<QSettings>(m_basedir % m_dbdir % "/" % shortName % ".info", QSettings::IniFormat);
entry->setProperty("profileName", shortName);
if (entry->value("app-id", -1).toInt() == -1) { // did not exist
2024-02-18 20:58:27 +01:00
if (behavior == OnlyExisting)
2024-03-06 11:48:16 +01:00
return entry;
// ok, then create a new one with a fresh id.
entry->setValue("app-id", m_nextJailId++);
2021-05-20 19:08:42 +02:00
}
2024-03-06 11:48:16 +01:00
return entry;
2021-05-20 19:08:42 +02:00
}
2024-02-15 23:39:04 +01:00
2024-02-25 16:21:41 +01:00
QList<IsolationManager::ProfileInfo> IsolationManager::listProfiles() const
2024-02-18 20:58:27 +01:00
{
2024-02-25 16:21:41 +01:00
QList<ProfileInfo> answer;
2024-02-18 20:58:27 +01:00
QDirIterator iter(m_basedir % m_dbdir);
while (iter.hasNext()) {
2024-02-25 16:21:41 +01:00
if (!iter.next().endsWith(".info"))
continue;
const auto infoFile = iter.fileName();
ProfileInfo pi;
pi.name = infoFile.left(infoFile.size() - 5); // filename, but chop off the extension
2024-02-25 19:48:41 +01:00
pi.name.replace("%2f", "/"); // reverse internal encoding
pi.name.replace("%%", "%");
2024-02-25 16:21:41 +01:00
QSettings entry(iter.filePath(), QSettings::IniFormat);
pi.jailId = entry.value("app-id", 0).toInt();
if (pi.jailId > 1) { // ignore invalid profile entries
2024-03-06 12:17:02 +01:00
pi.exe = entry.value("path-to-exe").toString();
2024-02-25 23:29:33 +01:00
QString appStateFile = stateFile(pi.jailId);
2024-02-25 19:48:41 +01:00
if (QFile::exists(appStateFile)) {
QSettings appState(appStateFile, QSettings::IniFormat);
pi.lastRun = appState.value("start").toDateTime();
int pid = appState.value("pid").toInt();
if (pid)
pi.active = QFile::exists(QString("/proc/%1").arg(pid));
}
2024-02-25 16:21:41 +01:00
answer.append(pi);
2024-02-18 20:58:27 +01:00
}
}
return answer;
}
QDir IsolationManager::dbDir() const
2024-02-18 20:58:27 +01:00
{
return QDir(m_basedir % m_dbdir);
}
2024-02-25 23:29:33 +01:00
QString IsolationManager::stateFile(int jailId) const
2024-02-25 19:48:41 +01:00
{
2024-02-25 23:29:33 +01:00
return QString (m_basedir % "/1/state-" % QString::number(jailId) % ".info");
}
QString IsolationManager::pipeFilePath(int jailId) const
{
return QString(m_basedir % QString::number(jailId) % "/.iso-pipe");
}
QString IsolationManager::jailDir(int jailId) const
{
return QString(m_basedir % QString::number(jailId));
2024-02-25 19:48:41 +01:00
}
void IsolationManager::applyRules(AppEntry &context, Message &message, const QString &ruleFile) const
2024-02-15 23:39:04 +01:00
{
QFile in(ruleFile);
if (!in.open(QIODevice::ReadOnly))
throw RulesError("Rules could not be read", ruleFile);
2024-02-18 00:22:50 +01:00
bool inIfBlock = false;
bool ifWasTrue = false;
2024-02-15 23:39:04 +01:00
int lineNum = 0;
2026-04-11 14:45:40 +02:00
for (const QString line_ : in.readAll().split('\n')) {
2024-02-15 23:39:04 +01:00
auto line = line_.trimmed();
++lineNum;
if (line.isEmpty() || line.startsWith('#'))
continue;
auto items = line.split(' ', Qt::SkipEmptyParts);
assert(!items.isEmpty());
// the try concept means that commands failing are seen as a problem
// and we stop the processing and fail to start the application if the
// command didn't succeed.
bool isATry = false;
if (items.at(0) == "try") {
items.takeFirst();
isATry = true;
}
2024-02-18 00:22:50 +01:00
if (inIfBlock) {
if (items.size() >= 2 && items.at(0) == "if")
throw RulesError("nested if detected", ruleFile);
if (items.size() == 1 && items.at(0) == "endif") {
inIfBlock = false;
continue;
}
if (!ifWasTrue)
continue;
} else if (items.at(0) == "endif") {
throw RulesError("endif without if found");
}
2024-02-15 23:39:04 +01:00
// bind takes 2 arguments.
if (items.size() == 3 && items.at(0) == "bind") {
message.setTry(isATry);
2024-02-17 18:11:48 +01:00
message.addRemount(
expandVars(context, items.at(1)).toStdString(),
expandVars(context, items.at(2)).toStdString());
2024-02-15 23:39:04 +01:00
}
else if (items.size() == 2 && items.at(0) == "umount") {
message.setTry(isATry);
2024-02-17 18:11:48 +01:00
message.addUmountPoint(expandVars(context, items.at(1)).toStdString());
2024-02-15 23:39:04 +01:00
}
else if (items.size() == 2 && items.at(0) == "tmpfs") {
message.setTry(isATry);
2024-02-17 18:11:48 +01:00
message.addMountTmpDir(expandVars(context, items.at(1)).toStdString());
2024-02-15 23:39:04 +01:00
}
2024-02-16 16:54:09 +01:00
else if (items.size() == 3 && items.at(0) == "copy") {
message.setTry(isATry);
2024-02-17 18:11:48 +01:00
message.addCopy(
expandVars(context, items.at(1)).toStdString(),
expandVars(context, items.at(2)).toStdString());
}
else if (items.size() == 2 && items.at(0) == "setEnv") {
if (isATry)
throw RulesError("try not allowed in front of setEnv");
auto var = expandVars(context, items.at(1));
if (var.indexOf('=') == -1)
throw RulesError("setEnv arg needs to be of name=value");
message.addEnvToSet(var.toStdString());
}
2024-02-19 10:47:36 +01:00
else if (items.size() == 3 && items.at(0) == "setPermissionDefault") {
if (!context.isKnownPermission(items.at(1)))
throw RulesError("unknown permission named");
if (items.at(2) != "allowed" && items.at(2) != "denied")
throw RulesError("default permission unknown. Use allowed or denied.");
context.defaults[items.at(1)] = (items.at(2) == "allowed");
}
2024-02-17 18:11:48 +01:00
else if (items.size() == 2 && items.at(0) == "unsetEnv") {
if (isATry)
throw RulesError("try not allowed in front of setEnv");
auto var = expandVars(context, items.at(1));
if (var.indexOf('=') != -1)
throw RulesError("unsetEnv arg can not have a '=' sign");
message.addEnvToUnset(var.toStdString());
2024-02-16 16:54:09 +01:00
}
2024-02-18 00:22:50 +01:00
else if (items.size() >= 2 && items.at(0) == "if") {
bool negative = false;
int index = 2;
if (items.at(1) == "denied") {
negative = true;
} else if (items.at(1) != "allowed") {
index = 1;
}
if (items.size() != index + 1)
throw RulesError("malformed if statement", ruleFile);
inIfBlock = true;
ifWasTrue = context.isAllowed(items.at(index));
if (negative) // invert result
ifWasTrue = !ifWasTrue;
}
2024-02-19 10:47:36 +01:00
else if (items.size() == 1 && items.at(0) == "execute-apprules") {
2024-03-06 11:48:16 +01:00
QString rules = m_rulesDir + context.profileName + ".rules";
2024-02-19 10:47:36 +01:00
// also check we're not recursing.
// first check if we have an app-specific one.
if (rules == ruleFile || !QFileInfo::exists(rules))
rules = m_rulesDir + "default.rules";
if (rules == ruleFile)
throw RulesError("Recursive execute-apprules found");
applyRules(context, message, rules);
}
2024-02-15 23:39:04 +01:00
else {
QString a = QString("Rule-parsing failure on line %1").arg(lineNum);
std::string latin1(a.toStdString());
throw RulesError(latin1.c_str(), ruleFile);
}
}
}
2024-02-16 16:54:09 +01:00
QString IsolationManager::expandVars(const AppEntry &context, const QString &path_) const
2024-02-16 16:54:09 +01:00
{
QString path(path_);
2024-02-17 18:11:48 +01:00
int index = path.indexOf("$APPHOME");
2024-02-16 16:54:09 +01:00
if (index != -1
2024-02-17 18:11:48 +01:00
&& (index <= 0 || path.at(index - 1) != '\\')) {
path = path.left(index) + QDir::homePath()
2024-02-19 19:52:24 +01:00
+ QString("/.local/jails/%1").arg(context.appId)
2024-02-17 18:11:48 +01:00
+ path.mid(index + 8);
}
index = path.indexOf("$HOME");
if (index != -1
&& (index <= 0 || path.at(index - 1) != '\\')) {
path = path.left(index) + QDir::homePath() + path.mid(index + 5);
2024-02-16 16:54:09 +01:00
}
2024-02-19 12:07:37 +01:00
index = path.indexOf("$USERID");
if (index != -1
&& (index <= 0 || path.at(index - 1) != '\\')) {
path = path.left(index) + QString::number(getuid()) + path.mid(index + 7);
}
2024-02-21 11:21:37 +01:00
index = path.indexOf("$JAILID");
if (index != -1
&& (index <= 0 || path.at(index - 1) != '\\')) {
path = path.left(index) + QString::number(context.appId) + path.mid(index + 7);
}
2024-02-16 16:54:09 +01:00
return path;
}
2024-02-18 00:22:50 +01:00
QString IsolationManager::rulesDir() const
2024-02-19 09:44:46 +01:00
{
return m_rulesDir;
}
void IsolationManager::setRulesDir(const QString &dir)
2024-02-19 09:44:46 +01:00
{
m_rulesDir = dir;
if (!m_rulesDir.endsWith('/'))
m_rulesDir += "/";
}
2024-02-25 19:22:08 +01:00
void IsolationManager::receivedMessageFromRunner(const QByteArray &data)
{
2024-02-25 23:29:33 +01:00
QString line_ = QString::fromLatin1(data);
QStringView line(line_);
2024-02-25 19:22:08 +01:00
int index = line.indexOf(' ');
if (index == -1) {
qWarning() << "Malformed message from runner";
return;
}
bool ok;
2024-02-25 23:29:33 +01:00
const int appId = line.left(index).toInt(&ok);
2024-02-25 19:22:08 +01:00
if (!ok || appId <= 1) {
qWarning() << "Malformed message from runner";
return;
}
2024-02-25 23:29:33 +01:00
const QString stateInfoFilename = stateFile(appId);
QSettings runInfo(stateInfoFilename, QSettings::IniFormat);
2024-02-25 19:22:08 +01:00
2024-02-25 23:29:33 +01:00
auto rest = line.mid(index + 1);
const auto pid = rest.toLong();
if (pid) {
runInfo.setValue("pid", rest.toString());
} else {
runInfo.setValue("error", rest.toString());
}
2024-02-25 19:22:08 +01:00
}
bool IsolationManager::AppEntry::isAllowed(const QString &tag) const
2024-02-18 00:22:50 +01:00
{
2024-02-19 10:47:36 +01:00
auto iter = defaults.find(tag);
2024-02-18 00:22:50 +01:00
bool defaultAnswer = true;
2024-02-19 10:47:36 +01:00
if (iter != defaults.end())
defaultAnswer = *iter;
2024-02-18 00:22:50 +01:00
if (defaultAnswer && denied.contains(tag))
return false;
if (!defaultAnswer && allowed.contains(tag))
return true;
return defaultAnswer;
}
2024-02-18 20:58:27 +01:00
void IsolationManager::AppEntry::setDenied(const QStringList &entries)
2024-02-18 20:58:27 +01:00
{
denied.clear();
for (const auto &perm : entries) {
if (!isKnownPermission(perm))
continue;
denied.append(perm);
}
}
void IsolationManager::AppEntry::setAllowed(const QStringList &entries)
2024-02-18 20:58:27 +01:00
{
allowed.clear();
for (const auto &perm : entries) {
if (!isKnownPermission(perm))
continue;
allowed.append(perm);
}
}
bool IsolationManager::AppEntry::isKnownPermission(const QString &perm) const
2024-02-18 20:58:27 +01:00
{
2024-02-20 19:14:25 +01:00
if (perm == "ssh" || perm == "git" || perm == "homedir" || perm == "media"
2026-04-11 14:45:40 +02:00
|| perm == "dbus" || perm == "dbus-system" || perm == "audio"
2024-05-02 23:17:17 +02:00
|| perm == "docker")
2024-02-18 20:58:27 +01:00
return true;
return false;
}
2024-02-25 23:29:33 +01:00
AutoDeleter::AutoDeleter(IsolationManager::AppEntry appEntry, IsolationManager *parent)
: QObject(parent),
2026-04-11 14:45:40 +02:00
m_parent(parent),
m_jail(appEntry)
2024-02-25 23:29:33 +01:00
{
assert(parent);
assert(m_jail.appId > 1);
// wait a little while, as it may take a couple more milliseconds
// to actually create the pipe.
QTimer::singleShot(1, this, SLOT(startMonitor()));
connect (&m_watcher, SIGNAL(fileChanged(QString)),
this, SLOT(jailClosed(QString)));
}
void AutoDeleter::startMonitor()
{
++m_try;
bool ok = m_watcher.addPath(m_parent->pipeFilePath(m_jail.appId));
if (!ok) {
// it fails if the file isn't there. Likely due to us trying
// too fast and the child process hasn't created the pipe yet.
if (m_try > 10) {
deleteLater();
return;
}
// try again soon.
QTimer::singleShot(5 * m_try, this, SLOT(startMonitor()));
}
}
void AutoDeleter::jailClosed(const QString &pipeFile)
{
2024-03-06 11:48:16 +01:00
bool ok = QFile::remove(m_parent->dbDir().absoluteFilePath(m_jail.profileName + ".info"));
2024-02-25 23:29:33 +01:00
if (!ok)
qWarning() << "Auto-remove: Failed to remove db file";
ok = QFile::remove(m_parent->stateFile(m_jail.appId));
if (!ok)
qWarning() << "Auto-remove: Failed to remove state";
QDir jailDir(m_parent->jailDir(m_jail.appId));
ok = jailDir.removeRecursively();
if (!ok)
qWarning() << "Auto-remove: Failed to remove jail-dir";
deleteLater();
}
2024-05-17 11:39:44 +02:00
DelayedApp::DelayedApp(IsolationManager::AppEntry appEntry, const QStringList &arguments, IsolationManager *parent)
2026-04-11 14:45:40 +02:00
: m_parent(parent),
m_jail(appEntry),
2024-05-17 11:39:44 +02:00
m_arguments(arguments)
{
assert(parent);
}
void DelayedApp::askPassword()
{
auto id = new QInputDialog();
m_win = id;
id->setInputMode(QInputDialog::TextInput);
id->setTextEchoMode(QLineEdit::Password);
id->setLabelText("ISO password:");
id->setWindowTitle("Jail Requires Password");
id->open(this, SLOT(passwordEntered(QString)));
connect (id, SIGNAL(rejected()), this, SLOT(cancelPressed()));
}
void DelayedApp::cancelPressed()
{
qWarning() << "canceled";
m_win->deleteLater();
deleteLater();
}
void DelayedApp::passwordEntered(const QString &text)
{
m_win->deleteLater();
m_jail.jailPassword = text;
m_parent->startApplicationRequest(m_jail, m_arguments);
deleteLater();
}