Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packaging/arch/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pkgdesc="Qt-based Linux port of Safe Exam Browser"
arch=('x86_64')
url="https://github.com/example/safe-exam-browser-linux"
license=('MPL2')
depends=('qt6-base' 'qt6-webengine' 'zlib' 'hicolor-icon-theme' 'shared-mime-info' 'desktop-file-utils')
depends=('qt6-base' 'qt6-webengine' 'zlib' 'hicolor-icon-theme' 'shared-mime-info' 'desktop-file-utils' 'polkit')
makedepends=('qt6-tools' 'gcc' 'make')
source=("safe-exam-browser-${pkgver}.tar.gz")
sha256sums=('SKIP')
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Section: education
Priority: optional
Architecture: amd64
Maintainer: SEB Linux contributors
Depends: libqt6core6, libqt6gui6, libqt6network6, libqt6webenginecore6, libqt6webenginewidgets6, shared-mime-info
Depends: libqt6core6, libqt6gui6, libqt6network6, libqt6webenginecore6, libqt6webenginewidgets6, shared-mime-info, pkexec
Description: Safe Exam Browser Linux Qt port
Qt-based Safe Exam Browser launcher for Linux with .seb file and sebs:// support.
EOF
Expand Down
166 changes: 163 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,108 @@
#include <QLineEdit>
#include <QTextStream>
#include <QUrl>
#include <QProcess>
#include <QProcessEnvironment>
#include <QFileInfo>

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/vt.h>
#include <linux/kd.h>
#include <signal.h>
namespace {

int g_tty0_fd = -1;
int g_new_tty_fd = -1;
int g_original_vt = -1;

void cleanup_vt_and_exit() {
if (g_new_tty_fd >= 0) {
ioctl(g_new_tty_fd, KDSETMODE, KD_TEXT);
}
if (g_tty0_fd >= 0) {
int target_vt = (g_original_vt > 0) ? g_original_vt : 1;
ioctl(g_tty0_fd, VT_ACTIVATE, target_vt);
ioctl(g_tty0_fd, VT_WAITACTIVE, target_vt);
}
if (g_new_tty_fd >= 0) close(g_new_tty_fd);
if (g_tty0_fd >= 0) close(g_tty0_fd);
}

void barebones_sig_handler(int signum) {
cleanup_vt_and_exit();
if (signum == SIGSEGV || signum == SIGABRT || signum == SIGFPE || signum == SIGILL) {
signal(signum, SIG_DFL);
raise(signum);
} else {
_exit(1);
}
}

bool setup_barebones_vt() {
signal(SIGINT, barebones_sig_handler);
signal(SIGTERM, barebones_sig_handler);
signal(SIGSEGV, barebones_sig_handler);
signal(SIGABRT, barebones_sig_handler);
signal(SIGILL, barebones_sig_handler);
signal(SIGFPE, barebones_sig_handler);

g_tty0_fd = open("/dev/tty0", O_RDWR);
if (g_tty0_fd < 0) {
qWarning() << "Could not open /dev/tty0. Did you run the app as root?";
return false;
}

int free_vt = -1;
if (ioctl(g_tty0_fd, VT_OPENQRY, &free_vt) < 0 || free_vt == -1) {
qWarning() << "Could not find a free VT.";
close(g_tty0_fd);
g_tty0_fd = -1;
return false;
}

char vt_name[20];
snprintf(vt_name, sizeof(vt_name), "/dev/tty%d", free_vt);
g_new_tty_fd = open(vt_name, O_RDWR);
if (g_new_tty_fd < 0) {
qWarning() << "Could not open VT" << vt_name;
close(g_tty0_fd);
g_tty0_fd = -1;
return false;
}

struct vt_stat vts;
if (ioctl(g_tty0_fd, VT_GETSTATE, &vts) == 0) {
g_original_vt = vts.v_active;
}

if (ioctl(g_tty0_fd, VT_ACTIVATE, free_vt) < 0 ||
ioctl(g_tty0_fd, VT_WAITACTIVE, free_vt) < 0) {
qWarning() << "Could not switch to VT" << free_vt;
close(g_new_tty_fd);
g_new_tty_fd = -1;
close(g_tty0_fd);
g_tty0_fd = -1;
return false;
}

if (ioctl(g_new_tty_fd, KDSETMODE, KD_GRAPHICS) < 0) {
qWarning() << "Error setting graphics mode on VT" << free_vt;
int target_vt = (g_original_vt > 0) ? g_original_vt : 1;
ioctl(g_tty0_fd, VT_ACTIVATE, target_vt);
ioctl(g_tty0_fd, VT_WAITACTIVE, target_vt);
close(g_new_tty_fd);
g_new_tty_fd = -1;
close(g_tty0_fd);
g_tty0_fd = -1;
return false;
}

atexit(cleanup_vt_and_exit);
return true;
}

QString findConfigPath(int argc, char *argv[])
{
for (int index = 1; index < argc; ++index) {
Expand All @@ -37,6 +136,18 @@ void applyEarlyEnvironment(int argc, char *argv[])
settings = loaded.settings;
}
}
for (int i = 0; i < argc; ++i) { // we have to use a loop because the command line parser was not yet loaded.
if (QString::fromLocal8Bit(argv[i]) == QStringLiteral("--anti-cheat")) {
if (!setup_barebones_vt()) {
qCritical() << "Anti-cheat VT setup failed; aborting.";
_exit(1);
}
qputenv("QT_QPA_PLATFORM", "linuxfb");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--no-sandbox");
qputenv("QT_QUICK_BACKEND", "software");
break;
}
}

seb::browser::applyWebEngineEnvironment(settings);
}
Expand Down Expand Up @@ -73,7 +184,9 @@ void applyCommandLineOverrides(const QCommandLineParser &parser, seb::SebSetting
if (parser.isSet("windowed")) {
settings.browser.mainWindow.fullScreenMode = false;
}

if (parser.isSet("fullscreen")){
settings.browser.mainWindow.fullScreenMode = true;
}
if (parser.isSet("always-on-top")) {
settings.browser.mainWindow.alwaysOnTop = true;
}
Expand All @@ -94,7 +207,8 @@ int main(int argc, char *argv[])
applyEarlyEnvironment(argc, argv);

QApplication app(argc, argv);
const QIcon appIcon(QStringLiteral(":/assets/icons/safe-exam-browser.png"));

const QIcon appIcon(QStringLiteral(":/assets/icons/safe-exam-browser.png"));
app.setWindowIcon(appIcon);
app.setDesktopFileName(QStringLiteral("safe-exam-browser"));
QCoreApplication::setApplicationName(QStringLiteral("Safe Exam Browser"));
Expand Down Expand Up @@ -127,13 +241,17 @@ int main(int argc, char *argv[])
QStringLiteral("allow-devtools"),
QStringLiteral("Enable the developer tools shortcut (F12).")));
parser.addOption(QCommandLineOption(QStringLiteral("windowed"), QStringLiteral("Force the main window to stay windowed.")));
parser.addOption(QCommandLineOption(QStringLiteral("fullscreen"), QStringLiteral("Force the main window to be fullscreen.")));
parser.addOption(QCommandLineOption(QStringLiteral("always-on-top"), QStringLiteral("Keep the main window above other windows.")));
parser.addOption(QCommandLineOption(
QStringLiteral("disable-minimize"),
QStringLiteral("Prevent minimizing the main exam window.")));
parser.addOption(QCommandLineOption(
QStringLiteral("disable-quit"),
QStringLiteral("Disable manual termination even if the configuration allows it.")));
parser.addOption(QCommandLineOption(
QStringLiteral("anti-cheat"),
QStringLiteral("Enable anticheat mode.")));

parser.process(app);

Expand All @@ -144,9 +262,18 @@ int main(int argc, char *argv[])
? parser.value("config")
: (parser.positionalArguments().isEmpty() ? QString() : parser.positionalArguments().constFirst());

QString userPassword;
bool usedPassword = false;

QStringList warnings;
if (!resource.isEmpty()) {
const seb::ResourceLoadResult loaded = seb::loadSettingsFromResource(resource, [](bool hashed) {
const seb::ResourceLoadResult loaded = seb::loadSettingsFromResource(resource, [&userPassword, &usedPassword](bool hashed) {
if (qEnvironmentVariableIsSet("SEB_PASSWORD")) {
userPassword = QString::fromUtf8(qgetenv("SEB_PASSWORD"));
usedPassword = true;
return userPassword;
}
Comment on lines +270 to +275
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't pass the SEB password through pkexec env.

The pkexec env SEB_PASSWORD=... handoff exposes the password in the detached process command line and then keeps it in the child environment for the rest of the session. Please switch this back to a one-shot transport such as the previous SEB_PASSWORD_FILE temp-file flow instead of argv/environment transport.

Based on learnings: in src/main.cpp, the prior anticheat re-launch block intentionally used QTemporaryFile* tempFile with SEB_PASSWORD_FILE, and the child deleted that file after reading it.

Also applies to: 293-296

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.cpp` around lines 247 - 252, Replace the current
environment-argument transport of the SEB password with the previous one-shot
temp-file flow: stop setting SEB_PASSWORD via pkexec env and instead create a
QTemporaryFile (like the prior QTemporaryFile* tempFile approach), write the
password into it, pass only SEB_PASSWORD_FILE=<tempfile path> to the relaunched
child, and ensure the child code that reads the password (the lambda used by
seb::loadSettingsFromResource and any other re-launch paths referenced around
lines 293-296) reads from that file and deletes/unlinks the temporary file
immediately after reading; update locations that currently rely on
qgetenv("SEB_PASSWORD") to read and clear the temp-file mechanism instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password isnt that secret...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@incomplete-tree, understood — if the SEB password is not considered sensitive in your deployment context, the environment-variable transport is simpler. The main residual risk worth being aware of is that environment variables of a running process can be read by other processes owned by the same user (e.g. via /proc/<pid>/environ), and they persist for the lifetime of the child process rather than being consumed immediately. But if that's acceptable for your threat model, no objection.


✏️ Learnings added
Learnt from: incomplete-tree
Repo: Jvr2022/seb-linux PR: 5
File: src/main.cpp:247-252
Timestamp: 2026-03-22T13:58:54.431Z
Learning: In `src/main.cpp` of the `seb-linux` project (C++/Qt), the SEB password is intentionally passed to the pkexec-relaunched anti-cheat child process via the `SEB_PASSWORD` environment variable (using `pkexec env SEB_PASSWORD=...`). The project author considers this acceptable — do not flag environment-variable password transport as a security issue in this codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: incomplete-tree
Repo: Jvr2022/seb-linux PR: 4
File: src/main.cpp:316-338
Timestamp: 2026-03-22T13:04:35.013Z
Learning: In `src/main.cpp` of the `seb-linux` project (C++/Qt), the `QTemporaryFile* tempFile` created in the anticheat re-launch block (around the `startDetached()` call) is intentionally not deleted on the success path. The parent process exits immediately (`return 0`) and the child process (re-launched with `--anti-cheat` via `pkexec`) is responsible for deleting the file via `file.remove()` inside the `SEB_PASSWORD_FILE` password callback. This is by design; do not flag it as a leak.

Learnt from: incomplete-tree
Repo: Jvr2022/seb-linux PR: 3
File: src/seb_session.cpp:39-79
Timestamp: 2026-03-19T18:39:47.748Z
Learning: In `src/seb_session.cpp` of the `seb-linux` project (C++/Qt), the `getCachedSebVersion()` function intentionally performs a blocking HTTP fetch (with a 5 s timeout) to `https://api.github.com/repos/SafeExamBrowser/seb-win-refactoring/releases/latest` during startup and advertises the remote tag in the `SEB/x.y.z` User-Agent token, rather than using the locally shipped application version. This behaviour is by design; do not flag it as an issue.


bool accepted = false;
const QString password = QInputDialog::getText(
nullptr,
Expand All @@ -157,6 +284,10 @@ int main(int argc, char *argv[])
QLineEdit::Password,
QString(),
&accepted);
if (accepted) {
userPassword = password;
usedPassword = true;
}
return accepted ? password : QString();
});
if (!loaded.ok) {
Expand All @@ -170,6 +301,35 @@ int main(int argc, char *argv[])

applyCommandLineOverrides(parser, settings);

if (!parser.isSet("anti-cheat")) {
if (settings.browser.mainWindow.fullScreenMode && settings.browser.mainWindow.alwaysOnTop) {
QStringList args = QCoreApplication::arguments();
args.removeFirst(); // Remove executable path
if (!args.contains(QStringLiteral("--anti-cheat"))) {
args.prepend(QStringLiteral("--anti-cheat"));
}

QProcess child;
child.setProgram(QStringLiteral("pkexec"));

QStringList pkexecArgs;
pkexecArgs << QStringLiteral("--keep-cwd");
if (usedPassword) {
pkexecArgs << QStringLiteral("env");
pkexecArgs << (QStringLiteral("SEB_PASSWORD=") + userPassword);
Comment on lines +318 to +319
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid putting the SEB password in pkexec argv

When an encrypted .seb file triggers the anti-cheat relaunch, this builds pkexec env SEB_PASSWORD=..., which copies the plaintext config/admin password into the detached process' argument vector. That value is observable through ps or /proc/*/cmdline while the polkit prompt is open, so local users can recover a secret that was originally entered in a password dialog.

Useful? React with 👍 / 👎.

}
pkexecArgs << QCoreApplication::applicationFilePath();
pkexecArgs << args;

child.setArguments(pkexecArgs);

if (child.startDetached()) {
return 0;
}
return 1;
}
}

AppController controller;
QString launchError;
if (!controller.launchResolved(settings, warnings, &launchError)) {
Expand Down