2021-05-20 12:43:04 +02:00
|
|
|
#include "Message.h"
|
2024-02-19 12:52:17 +01:00
|
|
|
#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
|
|
|
|
2024-02-19 12:52:17 +01: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
|
|
|
{
|
2024-02-19 12:52:17 +01: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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +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
|
|
|
|
2024-02-19 12:52:17 +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
|
|
|
|
2024-02-19 12:52:17 +01:00
|
|
|
QString IsolationManager::rulesDir() const
|
2024-02-19 09:44:46 +01:00
|
|
|
{
|
|
|
|
|
return m_rulesDir;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +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
|
|
|
|
2024-02-19 12:52:17 +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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 12:52:17 +01:00
|
|
|
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();
|
|
|
|
|
}
|