From 26617d790450b5325e5c6d2d7ed11e277f880850 Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 13:12:21 +0100 Subject: [PATCH 1/9] Add linux VT anticheat --- src/main.cpp | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index f7a3d21..73d0c18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,9 +10,82 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace { +int g_tty0_fd = -1; +int g_new_tty_fd = -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) { + ioctl(g_tty0_fd, VT_ACTIVATE, 1); + ioctl(g_tty0_fd, VT_WAITACTIVE, 1); + } + 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); + } +} + +void 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; + } + + int free_vt = -1; + if (ioctl(g_tty0_fd, VT_OPENQRY, &free_vt) < 0 || free_vt == -1) { + qWarning() << "Could not find a free VT."; + return; + } + + 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; + return; + } + + ioctl(g_tty0_fd, VT_ACTIVATE, free_vt); + ioctl(g_tty0_fd, VT_WAITACTIVE, free_vt); + + if (ioctl(g_new_tty_fd, KDSETMODE, KD_GRAPHICS) < 0) { + qWarning() << "Error setting graphics mode on VT" << free_vt; + } + + atexit(cleanup_vt_and_exit); +} + QString findConfigPath(int argc, char *argv[]) { for (int index = 1; index < argc; ++index) { @@ -37,6 +110,15 @@ 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")) { + setup_barebones_vt(); + qputenv("QT_QPA_PLATFORM", "linuxfb"); + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--no-sandbox"); + qputenv("QT_QUICK_BACKEND", "software"); + break; + } + } seb::browser::applyWebEngineEnvironment(settings); } @@ -73,7 +155,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; } @@ -94,7 +178,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")); @@ -127,6 +212,7 @@ 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"), @@ -134,6 +220,9 @@ int main(int argc, char *argv[]) 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); @@ -144,9 +233,27 @@ 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_FILE")) { + QString tempFilePath = QString::fromUtf8(qgetenv("SEB_PASSWORD_FILE")); + QFile file(tempFilePath); + if (file.open(QIODevice::ReadOnly)) { + QByteArray data = file.readAll(); + userPassword = QString::fromUtf8(data); + if (userPassword.endsWith('\n')) { + userPassword.chop(1); + } + file.remove(); // Unlink immediately + usedPassword = true; + return userPassword; + } + } + bool accepted = false; const QString password = QInputDialog::getText( nullptr, @@ -157,6 +264,10 @@ int main(int argc, char *argv[]) QLineEdit::Password, QString(), &accepted); + if (accepted) { + userPassword = password; + usedPassword = true; + } return accepted ? password : QString(); }); if (!loaded.ok) { @@ -170,6 +281,44 @@ 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; + QTemporaryFile *tempFile = nullptr; + if (usedPassword) { + tempFile = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/seb-pass-XXXXXX")); + tempFile->setAutoRemove(false); + if (tempFile->open()) { + tempFile->write(userPassword.toUtf8()); + tempFile->close(); + + pkexecArgs << QStringLiteral("env"); + pkexecArgs << (QStringLiteral("SEB_PASSWORD_FILE=") + tempFile->fileName()); + } + } + + pkexecArgs << QCoreApplication::applicationFilePath(); + pkexecArgs << args; + + child.setArguments(pkexecArgs); + + if (child.startDetached()) { + return 0; + } + delete tempFile; + return 1; + } + } + AppController controller; QString launchError; if (!controller.launchResolved(settings, warnings, &launchError)) { From 46e8d5177aec3f90c885d1aa9a39fbcf648455fb Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 14:03:14 +0100 Subject: [PATCH 2/9] fix OB1 error --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 73d0c18..8baff40 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,7 +110,7 @@ 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. + 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")) { setup_barebones_vt(); qputenv("QT_QPA_PLATFORM", "linuxfb"); From b1733f845c85c5ac617ca676357776f538874310 Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 14:08:13 +0100 Subject: [PATCH 3/9] env variable instead of temporary file --- src/main.cpp | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8baff40..d779d12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,8 +12,6 @@ #include #include #include -#include -#include #include #include @@ -239,19 +237,10 @@ int main(int argc, char *argv[]) QStringList warnings; if (!resource.isEmpty()) { const seb::ResourceLoadResult loaded = seb::loadSettingsFromResource(resource, [&userPassword, &usedPassword](bool hashed) { - if (qEnvironmentVariableIsSet("SEB_PASSWORD_FILE")) { - QString tempFilePath = QString::fromUtf8(qgetenv("SEB_PASSWORD_FILE")); - QFile file(tempFilePath); - if (file.open(QIODevice::ReadOnly)) { - QByteArray data = file.readAll(); - userPassword = QString::fromUtf8(data); - if (userPassword.endsWith('\n')) { - userPassword.chop(1); - } - file.remove(); // Unlink immediately - usedPassword = true; - return userPassword; - } + if (qEnvironmentVariableIsSet("SEB_PASSWORD")) { + userPassword = QString::fromUtf8(qgetenv("SEB_PASSWORD")); + usedPassword = true; + return userPassword; } bool accepted = false; @@ -293,17 +282,9 @@ int main(int argc, char *argv[]) child.setProgram(QStringLiteral("pkexec")); QStringList pkexecArgs; - QTemporaryFile *tempFile = nullptr; if (usedPassword) { - tempFile = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/seb-pass-XXXXXX")); - tempFile->setAutoRemove(false); - if (tempFile->open()) { - tempFile->write(userPassword.toUtf8()); - tempFile->close(); - - pkexecArgs << QStringLiteral("env"); - pkexecArgs << (QStringLiteral("SEB_PASSWORD_FILE=") + tempFile->fileName()); - } + pkexecArgs << QStringLiteral("env"); + pkexecArgs << (QStringLiteral("SEB_PASSWORD=") + userPassword); } pkexecArgs << QCoreApplication::applicationFilePath(); @@ -314,7 +295,6 @@ int main(int argc, char *argv[]) if (child.startDetached()) { return 0; } - delete tempFile; return 1; } } From d21da6d5f55c55c263bfcc9b87e1d6ce8411c4ed Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 14:11:56 +0100 Subject: [PATCH 4/9] add missing include --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index d779d12..2b69115 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include From 6cbaaace4ab9ea5c71efe3c1bfc910960f5bde58 Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 14:16:59 +0100 Subject: [PATCH 5/9] go back to original vt --- src/main.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 2b69115..f2b3bfe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,14 +24,16 @@ 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) { - ioctl(g_tty0_fd, VT_ACTIVATE, 1); - ioctl(g_tty0_fd, VT_WAITACTIVE, 1); + 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); @@ -75,6 +77,11 @@ void setup_barebones_vt() { return; } + struct vt_stat vts; + if (ioctl(g_tty0_fd, VT_GETSTATE, &vts) == 0) { + g_original_vt = vts.v_active; + } + ioctl(g_tty0_fd, VT_ACTIVATE, free_vt); ioctl(g_tty0_fd, VT_WAITACTIVE, free_vt); From be2667b09f308851fd27e212adef38aa69e9901d Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 14:59:37 +0100 Subject: [PATCH 6/9] if vt call fails, crash --- src/main.cpp | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index f2b3bfe..3fac96a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,7 +49,7 @@ void barebones_sig_handler(int signum) { } } -void setup_barebones_vt() { +bool setup_barebones_vt() { signal(SIGINT, barebones_sig_handler); signal(SIGTERM, barebones_sig_handler); signal(SIGSEGV, barebones_sig_handler); @@ -60,13 +60,15 @@ void setup_barebones_vt() { 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; + 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."; - return; + close(g_tty0_fd); + g_tty0_fd = -1; + return false; } char vt_name[20]; @@ -74,7 +76,9 @@ void setup_barebones_vt() { g_new_tty_fd = open(vt_name, O_RDWR); if (g_new_tty_fd < 0) { qWarning() << "Could not open VT" << vt_name; - return; + close(g_tty0_fd); + g_tty0_fd = -1; + return false; } struct vt_stat vts; @@ -82,14 +86,31 @@ void setup_barebones_vt() { g_original_vt = vts.v_active; } - ioctl(g_tty0_fd, VT_ACTIVATE, free_vt); - ioctl(g_tty0_fd, VT_WAITACTIVE, free_vt); + 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; + // Switch back to the original VT before giving up. + 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[]) @@ -118,7 +139,10 @@ void applyEarlyEnvironment(int argc, char *argv[]) } 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")) { - setup_barebones_vt(); + 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"); From 5f2a3449aadeb0348e66705373d51897491106a0 Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 15:24:57 +0100 Subject: [PATCH 7/9] add --keep-cwd --- src/main.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3fac96a..6ee632e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -98,7 +98,6 @@ bool setup_barebones_vt() { if (ioctl(g_new_tty_fd, KDSETMODE, KD_GRAPHICS) < 0) { qWarning() << "Error setting graphics mode on VT" << free_vt; - // Switch back to the original VT before giving up. 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); @@ -318,7 +317,7 @@ int main(int argc, char *argv[]) pkexecArgs << QStringLiteral("env"); pkexecArgs << (QStringLiteral("SEB_PASSWORD=") + userPassword); } - + pkexecArgs << QStringLiteral("--keep-cwd"); pkexecArgs << QCoreApplication::applicationFilePath(); pkexecArgs << args; From c9bce92dd66e8c48cd8a6e99c78fa11a04e269d6 Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 15:32:03 +0100 Subject: [PATCH 8/9] Add polkit as a runtime dependency. --- packaging/arch/PKGBUILD | 2 +- scripts/build-release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 02b05df..d5d9ae8 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -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') diff --git a/scripts/build-release.sh b/scripts/build-release.sh index c4a58a0..4d61f59 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -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 From 1604a9025f9d595a90b50b2a10df4fd632a22c0f Mon Sep 17 00:00:00 2001 From: Jouke van Dam Date: Sun, 22 Mar 2026 15:45:44 +0100 Subject: [PATCH 9/9] fix args --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 6ee632e..a28cfb8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -313,11 +313,11 @@ int main(int argc, char *argv[]) child.setProgram(QStringLiteral("pkexec")); QStringList pkexecArgs; + pkexecArgs << QStringLiteral("--keep-cwd"); if (usedPassword) { pkexecArgs << QStringLiteral("env"); pkexecArgs << (QStringLiteral("SEB_PASSWORD=") + userPassword); } - pkexecArgs << QStringLiteral("--keep-cwd"); pkexecArgs << QCoreApplication::applicationFilePath(); pkexecArgs << args;