From a1c3fb570fb87512946bec2a2839117f0ea33b09 Mon Sep 17 00:00:00 2001 From: Sindre Date: Sat, 3 Oct 2026 17:17:27 +0200 Subject: [PATCH 01/13] Add SessionController class Signed-off-by: Sindre --- .gitignore | 3 ++ shell/CMakeLists.txt | 5 +++ shell/src/session/sessionController.cpp | 41 +++++++++++++++++++++++++ shell/src/session/sessionController.h | 38 +++++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 shell/src/session/sessionController.cpp create mode 100644 shell/src/session/sessionController.h diff --git a/.gitignore b/.gitignore index e8f5548..70dda50 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ /.venv-docs/ /docs/_build/ +/shell/build/ +/shell/.cache/ + **/.DS_Store diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 0269757..e4074ee 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -18,7 +18,12 @@ set_source_files_properties(qml/Main.qml PROPERTIES QT_RESOURCE_ALIAS Main.qml) qt_add_qml_module(app URI Mosaic QML_FILES qml/Main.qml + SOURCES + src/session/sessionController.h + src/session/sessionController.cpp ) +target_include_directories(app PRIVATE src/session) + target_link_libraries(app PRIVATE Qt6::Quick) target_compile_options(app PRIVATE -Wall -Wextra) diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp new file mode 100644 index 0000000..e5b809b --- /dev/null +++ b/shell/src/session/sessionController.cpp @@ -0,0 +1,41 @@ +#include "sessionController.h" + +SessionController::SessionController(QObject *parent) : QObject(parent) {} + +QString SessionController::getErrorMsg() const { + return error_msg; +} + +bool SessionController::isLocked() const { + return m_state != State::UNLOCKED; +} + +void SessionController::lock() { + setState(State::LOCKED); +} + +void SessionController::requestUnlock(const QString &password) { + setState(State::AUTHENTICATING); + + //TODO Implement logic for checking password + if (password == "root") { + setState(State::UNLOCKED); + } else { + setState(State::LOCKED); + setErrorMsg("Incorrect Password"); + } +} + +void SessionController::setState(const State state) { + if (m_state == state) return; + + m_state = state; + emit stateChanged(); +} + +void SessionController::setErrorMsg(const QString &msg) { + if (error_msg == msg) return; + + error_msg = msg; + emit errorMsgChanged(); +} diff --git a/shell/src/session/sessionController.h b/shell/src/session/sessionController.h new file mode 100644 index 0000000..82173e8 --- /dev/null +++ b/shell/src/session/sessionController.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +class SessionController : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool locked READ isLocked NOTIFY stateChanged) + Q_PROPERTY(QString error_msg READ getErrorMsg NOTIFY errorMsgChanged) + + public: + enum class State{LOCKED, AUTHENTICATING, UNLOCKED}; + Q_ENUM(State) + + explicit SessionController(QObject *parent = nullptr); + + QString getErrorMsg() const; + bool isLocked() const; + + Q_INVOKABLE void lock(); + Q_INVOKABLE void requestUnlock(const QString &password); + + signals: + void stateChanged(); + void errorMsgChanged(); + + private: + State m_state = State::LOCKED; + QString error_msg; + + void setState(const State state); + void setErrorMsg(const QString &msg); +}; + From 89592cf7ce938dfef21455dccaaa6a65ffbfa37e Mon Sep 17 00:00:00 2001 From: Sindre Date: Sat, 3 Oct 2026 18:14:00 +0200 Subject: [PATCH 02/13] Add simple functional lockscreen Signed-off-by: Sindre --- shell/CMakeLists.txt | 10 ++++-- shell/qml/HomeScreen.qml | 12 +++++++ shell/qml/LockScreen.qml | 77 ++++++++++++++++++++++++++++++++++++++++ shell/qml/Main.qml | 7 ++-- shell/qml/Shell.qml | 23 ++++++++++++ 5 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 shell/qml/HomeScreen.qml create mode 100644 shell/qml/LockScreen.qml create mode 100644 shell/qml/Shell.qml diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index e4074ee..65b2207 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -find_package(Qt6 6.5 REQUIRED COMPONENTS Quick) +find_package(Qt6 6.5 REQUIRED COMPONENTS Quick QuickControls2) qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(app src/main.cpp) @@ -17,7 +17,11 @@ set_source_files_properties(qml/Main.qml PROPERTIES QT_RESOURCE_ALIAS Main.qml) qt_add_qml_module(app URI Mosaic - QML_FILES qml/Main.qml + QML_FILES + qml/Main.qml + qml/Shell.qml + qml/LockScreen.qml + qml/HomeScreen.qml SOURCES src/session/sessionController.h src/session/sessionController.cpp @@ -25,5 +29,5 @@ qt_add_qml_module(app target_include_directories(app PRIVATE src/session) -target_link_libraries(app PRIVATE Qt6::Quick) +target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2) target_compile_options(app PRIVATE -Wall -Wextra) diff --git a/shell/qml/HomeScreen.qml b/shell/qml/HomeScreen.qml new file mode 100644 index 0000000..9d7ec86 --- /dev/null +++ b/shell/qml/HomeScreen.qml @@ -0,0 +1,12 @@ +import QtQuick + +Item { + anchors.fill: parent + + Text { + text: "HOME" + color: "black" + font.pixelSize: 44 + anchors.centerIn: parent + } +} diff --git a/shell/qml/LockScreen.qml b/shell/qml/LockScreen.qml new file mode 100644 index 0000000..6979276 --- /dev/null +++ b/shell/qml/LockScreen.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + id: root + color: "white" + + property string errorMessage: "" + signal unlockRequested(string password) + + function resetPasswordInput() { + passwordField.clear() + + if (root.visible) + passwordField.forceActiveFocus() + } + + function submitPassword() { + const password = passwordField.text + + if (password.length === 0) + return + + passwordField.clear() + root.unlockRequested(password) + + Qt.callLater(root.resetPasswordInput) + } + + Component.onCompleted: Qt.callLater(root.resetPasswordInput) + onVisibleChanged: Qt.callLater(root.resetPasswordInput) + + Column { + anchors.centerIn: parent + width: Math.max(0, Math.min(320, root.width - 40)) + spacing: 16 + + Label { + text: qsTr("Mosaic") + color: "Black" + font.pixelSize: 44 + anchors.horizontalCenter: parent.horizontalCenter + } + + TextField { + id: passwordField + width: parent.width + height: width / 3 + + + horizontalAlignment: TextInput.AlignHCenter + placeholderText: qsTr("****") + + font.pixelSize: 34 + color: "black" + background: Rectangle { + color: "white" + border.width: 2 + border.color: "gainsboro" + } + + echoMode: TextInput.Password + inputMethodHints: Qt.ImhHiddenText | Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + + onAccepted: root.submitPassword() + } + + Label { + width: parent.width + text: root.errorMessage + visible: text.length > 0 + color: "red" + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/shell/qml/Main.qml b/shell/qml/Main.qml index b33606a..85d9b21 100644 --- a/shell/qml/Main.qml +++ b/shell/qml/Main.qml @@ -8,10 +8,7 @@ Window { title: qsTr("Mosaic") color: "white" - Text { - text: "Mosaic" - color: "black" - font.pixelSize: 48 - anchors.centerIn: parent + Shell { + anchors.fill: parent } } diff --git a/shell/qml/Shell.qml b/shell/qml/Shell.qml new file mode 100644 index 0000000..6b55270 --- /dev/null +++ b/shell/qml/Shell.qml @@ -0,0 +1,23 @@ +import QtQuick +import Mosaic + +Item { + HomeScreen { + anchors.fill: parent + visible: !SessionController.locked + enabled: !SessionController.locked + } + + LockScreen { + anchors.fill: parent + z: 1 + + visible: SessionController.locked + enabled: SessionController.locked + errorMessage: SessionController.error_msg + + onUnlockRequested: (password) => { + SessionController.requestUnlock(password) + } + } +} From 10c44173cdc31452125425b71b1240400be4b92c Mon Sep 17 00:00:00 2001 From: Sindre Date: Sat, 3 Oct 2026 18:19:04 +0200 Subject: [PATCH 03/13] Add clock to lock screen Signed-off-by: Sindre --- shell/qml/LockScreen.qml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shell/qml/LockScreen.qml b/shell/qml/LockScreen.qml index 6979276..8707bdf 100644 --- a/shell/qml/LockScreen.qml +++ b/shell/qml/LockScreen.qml @@ -36,10 +36,21 @@ Rectangle { spacing: 16 Label { - text: qsTr("Mosaic") + id: clockLabel + property date currentTime: new Date() + + text: Qt.formatTime(currentTime, "hh:mm") color: "Black" - font.pixelSize: 44 + font.pixelSize: 64 anchors.horizontalCenter: parent.horizontalCenter + + Timer { + interval: 1000 + repeat: true + running: root.visible + triggeredOnStart: true + onTriggered: clockLabel.currentTime = new Date() + } } TextField { From 6efa7e6e3af1976c35a15fc1b68509fc120b8e54 Mon Sep 17 00:00:00 2001 From: Sindre Date: Mon, 5 Oct 2026 16:56:10 +0200 Subject: [PATCH 04/13] Add AuthenticationBackend.h interface Signed-off-by: Sindre --- shell/CMakeLists.txt | 2 ++ shell/src/auth/AuthenticationBackend.h | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 shell/src/auth/AuthenticationBackend.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index 65b2207..f4b62da 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -25,6 +25,8 @@ qt_add_qml_module(app SOURCES src/session/sessionController.h src/session/sessionController.cpp + + src/auth/AuthenticationBackend.h ) target_include_directories(app PRIVATE src/session) diff --git a/shell/src/auth/AuthenticationBackend.h b/shell/src/auth/AuthenticationBackend.h new file mode 100644 index 0000000..80ec9f6 --- /dev/null +++ b/shell/src/auth/AuthenticationBackend.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class AuthenticationBackend : public QObject { + Q_OBJECT + + public: + enum class Result{SUCCESS, REJECTED, BACKENDFAILURE}; + Q_ENUM(Result) + + explicit AuthenticationBackend(QObject *parent = nullptr) : QObject(parent) {} + + virtual void authenticate(quint64 request_id, QString password) = 0; + + signals: + void authenticationFinished(quint64 request_id, Result result); +}; From ae7f6e76bc26e03f8c2e79090b41fe8122055387 Mon Sep 17 00:00:00 2001 From: Tmpecho Date: Mon, 5 Oct 2026 22:28:39 +0200 Subject: [PATCH 05/13] Add clang formatting --- shell/.clang-format | 104 ++++++++++++++++++++++++ shell/src/auth/AuthenticationBackend.h | 21 +++-- shell/src/main.cpp | 15 ++-- shell/src/session/sessionController.cpp | 60 ++++++++------ shell/src/session/sessionController.h | 50 ++++++------ 5 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 shell/.clang-format diff --git a/shell/.clang-format b/shell/.clang-format new file mode 100644 index 0000000..2d0acfc --- /dev/null +++ b/shell/.clang-format @@ -0,0 +1,104 @@ +# Generated by CLion for QT +BasedOnStyle: LLVM + +AccessModifierOffset: -8 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: DontAlign +AlignOperands: Align +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: "^ IWYU pragma:" +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - forever # avoids { wrapped to next line + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: "^ #include -class AuthenticationBackend : public QObject { - Q_OBJECT +class AuthenticationBackend : public QObject +{ + Q_OBJECT - public: - enum class Result{SUCCESS, REJECTED, BACKENDFAILURE}; - Q_ENUM(Result) +public: + enum class Result { SUCCESS, REJECTED, BACKENDFAILURE }; + Q_ENUM(Result) - explicit AuthenticationBackend(QObject *parent = nullptr) : QObject(parent) {} + explicit AuthenticationBackend(QObject *parent = nullptr) + : QObject(parent) + {} - virtual void authenticate(quint64 request_id, QString password) = 0; + virtual void authenticate(quint64 request_id, QString password) = 0; - signals: - void authenticationFinished(quint64 request_id, Result result); +signals: + void authenticationFinished(quint64 request_id, Result result); }; diff --git a/shell/src/main.cpp b/shell/src/main.cpp index 3c79bd5..8a56b47 100644 --- a/shell/src/main.cpp +++ b/shell/src/main.cpp @@ -1,14 +1,15 @@ #include #include -int main(int argc, char* argv[]) { - QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - engine.loadFromModule("Mosaic", "Main"); + engine.loadFromModule("Mosaic", "Main"); - return app.exec(); + return app.exec(); } diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp index e5b809b..15ddeb0 100644 --- a/shell/src/session/sessionController.cpp +++ b/shell/src/session/sessionController.cpp @@ -1,41 +1,51 @@ #include "sessionController.h" -SessionController::SessionController(QObject *parent) : QObject(parent) {} +SessionController::SessionController(QObject *parent) + : QObject(parent) +{} -QString SessionController::getErrorMsg() const { - return error_msg; +QString SessionController::getErrorMsg() const +{ + return error_msg; } -bool SessionController::isLocked() const { - return m_state != State::UNLOCKED; +bool SessionController::isLocked() const +{ + return m_state != State::UNLOCKED; } -void SessionController::lock() { - setState(State::LOCKED); +void SessionController::lock() +{ + setState(State::LOCKED); } -void SessionController::requestUnlock(const QString &password) { - setState(State::AUTHENTICATING); - - //TODO Implement logic for checking password - if (password == "root") { - setState(State::UNLOCKED); - } else { - setState(State::LOCKED); - setErrorMsg("Incorrect Password"); - } +void SessionController::requestUnlock(const QString &password) +{ + setState(State::AUTHENTICATING); + + // TODO Implement logic for checking password + if (password == "root") { + setState(State::UNLOCKED); + } else { + setState(State::LOCKED); + setErrorMsg("Incorrect Password"); + } } -void SessionController::setState(const State state) { - if (m_state == state) return; +void SessionController::setState(const State state) +{ + if (m_state == state) + return; - m_state = state; - emit stateChanged(); + m_state = state; + emit stateChanged(); } -void SessionController::setErrorMsg(const QString &msg) { - if (error_msg == msg) return; +void SessionController::setErrorMsg(const QString &msg) +{ + if (error_msg == msg) + return; - error_msg = msg; - emit errorMsgChanged(); + error_msg = msg; + emit errorMsgChanged(); } diff --git a/shell/src/session/sessionController.h b/shell/src/session/sessionController.h index 82173e8..672c1e4 100644 --- a/shell/src/session/sessionController.h +++ b/shell/src/session/sessionController.h @@ -1,38 +1,38 @@ #pragma once #include -#include #include +#include + +class SessionController : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON -class SessionController : public QObject { - Q_OBJECT - QML_ELEMENT - QML_SINGLETON + Q_PROPERTY(bool locked READ isLocked NOTIFY stateChanged) + Q_PROPERTY(QString error_msg READ getErrorMsg NOTIFY errorMsgChanged) - Q_PROPERTY(bool locked READ isLocked NOTIFY stateChanged) - Q_PROPERTY(QString error_msg READ getErrorMsg NOTIFY errorMsgChanged) +public: + enum class State { LOCKED, AUTHENTICATING, UNLOCKED }; + Q_ENUM(State) - public: - enum class State{LOCKED, AUTHENTICATING, UNLOCKED}; - Q_ENUM(State) + explicit SessionController(QObject *parent = nullptr); - explicit SessionController(QObject *parent = nullptr); + QString getErrorMsg() const; + bool isLocked() const; - QString getErrorMsg() const; - bool isLocked() const; + Q_INVOKABLE void lock(); + Q_INVOKABLE void requestUnlock(const QString &password); - Q_INVOKABLE void lock(); - Q_INVOKABLE void requestUnlock(const QString &password); +signals: + void stateChanged(); + void errorMsgChanged(); - signals: - void stateChanged(); - void errorMsgChanged(); +private: + State m_state = State::LOCKED; + QString error_msg; - private: - State m_state = State::LOCKED; - QString error_msg; - - void setState(const State state); - void setErrorMsg(const QString &msg); + void setState(const State state); + void setErrorMsg(const QString &msg); }; - From d09a5b58399807b482cb85943d716fd095a8cab6 Mon Sep 17 00:00:00 2001 From: Sindre Date: Tue, 6 Oct 2026 18:14:18 +0200 Subject: [PATCH 06/13] Add empty password check Signed-off-by: Sindre --- shell/src/session/sessionController.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp index 15ddeb0..29c504a 100644 --- a/shell/src/session/sessionController.cpp +++ b/shell/src/session/sessionController.cpp @@ -21,6 +21,11 @@ void SessionController::lock() void SessionController::requestUnlock(const QString &password) { + if (password.isEmpty()) { + setErrorMsg("Password cannot be empty"); + return; + } + setState(State::AUTHENTICATING); // TODO Implement logic for checking password From f97ed7f48bfe8b5dc1735a8b03423aeab82a5c7e Mon Sep 17 00:00:00 2001 From: Sindre Date: Tue, 6 Oct 2026 18:14:34 +0200 Subject: [PATCH 07/13] Add PamAuthenticationBackend Signed-off-by: Sindre --- shell/CMakeLists.txt | 12 +- shell/src/auth/PamAuthenticationBackend.cpp | 153 ++++++++++++++++++++ shell/src/auth/PamAuthenticationBackend.h | 37 +++++ 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 shell/src/auth/PamAuthenticationBackend.cpp create mode 100644 shell/src/auth/PamAuthenticationBackend.h diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index f4b62da..a7a3536 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -8,6 +8,12 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +find_path(PAM_INCLUDE_DIR NAMES security/pam_appl.h) +find_library(PAM_LIBRARY NAMES pam) +if(NOT PAM_INCLUDE_DIR OR NOT PAM_LIBRARY) + message(FATAL_ERROR "PAM development files not found. Install libpam0g-dev on Ubuntu, or provide PAM in the target sysroot.") +endif() + find_package(Qt6 6.5 REQUIRED COMPONENTS Quick QuickControls2) qt_standard_project_setup(REQUIRES 6.5) @@ -27,9 +33,11 @@ qt_add_qml_module(app src/session/sessionController.cpp src/auth/AuthenticationBackend.h + src/auth/PamAuthenticationBackend.h + src/auth/PamAuthenticationBackend.cpp ) -target_include_directories(app PRIVATE src/session) +target_include_directories(app PRIVATE src/session ${PAM_INCLUDE_DIR}) -target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2) +target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2 ${PAM_LIBRARY}) target_compile_options(app PRIVATE -Wall -Wextra) diff --git a/shell/src/auth/PamAuthenticationBackend.cpp b/shell/src/auth/PamAuthenticationBackend.cpp new file mode 100644 index 0000000..2de0f1c --- /dev/null +++ b/shell/src/auth/PamAuthenticationBackend.cpp @@ -0,0 +1,153 @@ +#include "PamAuthenticationBackend.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int PamAuthenticationBackend::conversationCallback( + int messageCount, const pam_message **messages, pam_response **responses, void *appdata) +{ + if (responses == nullptr) + return PAM_CONV_ERR; + + *responses = nullptr; + if (messageCount <= 0 || messageCount > PAM_MAX_NUM_MSG || messages == nullptr + || appdata == nullptr) + return PAM_CONV_ERR; + + const auto *context = static_cast(appdata); + auto *replies = static_cast( + std::calloc(static_cast(messageCount), sizeof(pam_response))); + if (replies == nullptr) + return PAM_BUF_ERR; + + const auto fail = [replies, messageCount](int status) { + for (int i = 0; i < messageCount; ++i) + std::free(replies[i].resp); + std::free(replies); + return status; + }; + + for (int i = 0; i < messageCount; ++i) { + if (messages[i] == nullptr) + return fail(PAM_CONV_ERR); + + const QByteArray *answer = nullptr; + switch (messages[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + answer = &context->password; + break; + case PAM_PROMPT_ECHO_ON: + answer = &context->username; + break; + case PAM_TEXT_INFO: + case PAM_ERROR_MSG: + continue; + default: + return fail(PAM_CONV_ERR); + } + + const auto length = static_cast(answer->size()); + if (std::memchr(answer->constData(), '\0', length) != nullptr) + return fail(PAM_CONV_ERR); + + replies[i].resp = static_cast(std::malloc(length + 1)); + if (replies[i].resp == nullptr) + return fail(PAM_BUF_ERR); + + std::memcpy(replies[i].resp, answer->constData(), length); + replies[i].resp[length] = '\0'; + } + + *responses = replies; + return PAM_SUCCESS; +} + +//TODO make async +void PamAuthenticationBackend::authenticate(quint64 request_id, QString password) +{ + QByteArray username; + try { + username = getCurrentUsername(); + } catch (const std::exception &e) { + emit authenticationFinished(request_id, Result::BACKENDFAILURE); + return; + } + + struct ConversationContext context + { + username, password.toUtf8() + }; + + pam_conv conversation{conversationCallback, &context}; + + pam_handle_t *handle = nullptr; + + int status = pam_start("sudo", context.username.constData(), &conversation, &handle); + if (status != PAM_SUCCESS) { + emit authenticationFinished(request_id, Result::BACKENDFAILURE); + return; + } + + status = pam_authenticate(handle, PAM_DISALLOW_NULL_AUTHTOK); + if (status != PAM_SUCCESS) { + pam_end(handle, status); + emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); + return; + } + + status = pam_acct_mgmt(handle, 0); + if (status != PAM_SUCCESS) { + pam_end(handle, status); + emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); + return; + } + + status = pam_end(handle, status); + emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); +} + +QByteArray PamAuthenticationBackend::getCurrentUsername() +{ + passwd entry{}; + passwd *result = nullptr; + std::vector buffer(4096); + + int status; + while (true) { + status = getpwuid_r(getuid(), &entry, buffer.data(), buffer.size(), &result); + + if (status != ERANGE) + break; + + buffer.resize(buffer.size() * 2); + } + + if (status != 0 || result == nullptr) + throw std::runtime_error("Unable to resolve current user"); + + return QByteArray(entry.pw_name); +} + +AuthenticationBackend::Result PamAuthenticationBackend::resolveResultFromPamStatus(int status) +{ + switch (status) { + case PAM_AUTH_ERR: + case PAM_USER_UNKNOWN: + case PAM_MAXTRIES: + case PAM_PERM_DENIED: + case PAM_ACCT_EXPIRED: + case PAM_NEW_AUTHTOK_REQD: + case PAM_AUTHTOK_EXPIRED: + return Result::REJECTED; + case PAM_SUCCESS: + return Result::SUCCESS; + default: + return Result::BACKENDFAILURE; + } +} diff --git a/shell/src/auth/PamAuthenticationBackend.h b/shell/src/auth/PamAuthenticationBackend.h new file mode 100644 index 0000000..efc7517 --- /dev/null +++ b/shell/src/auth/PamAuthenticationBackend.h @@ -0,0 +1,37 @@ +#pragma once + +#include "AuthenticationBackend.h" + +#include +#include +#include + +struct pam_message; +struct pam_response; + +class PamAuthenticationBackend : public AuthenticationBackend +{ +public: + explicit PamAuthenticationBackend(QObject *parent = nullptr) + : AuthenticationBackend(parent) + {} + + void authenticate(quint64 request_id, QString password) override; + +private: + struct ConversationContext + { + QByteArray username; + QByteArray password; + }; + + QByteArray getCurrentUsername(); + + Result resolveResultFromPamStatus(int status); + + static int conversationCallback( + int messageCount, + const pam_message **messages, + pam_response **responses, + void *appdata); +}; From 4bf97e4c0858aa3e9ecd0cd3e211be43f79cee44 Mon Sep 17 00:00:00 2001 From: Sindre Date: Wed, 7 Oct 2026 16:03:25 +0200 Subject: [PATCH 08/13] Integrate AuthenticationBackend in SessionController Signed-off-by: Sindre --- shell/CMakeLists.txt | 2 +- shell/src/session/sessionController.cpp | 45 +++++++++++++++++++------ shell/src/session/sessionController.h | 4 +++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index a7a3536..a4b2932 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -37,7 +37,7 @@ qt_add_qml_module(app src/auth/PamAuthenticationBackend.cpp ) -target_include_directories(app PRIVATE src/session ${PAM_INCLUDE_DIR}) +target_include_directories(app PRIVATE src/session src/auth ${PAM_INCLUDE_DIR}) target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2 ${PAM_LIBRARY}) target_compile_options(app PRIVATE -Wall -Wextra) diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp index 29c504a..69e832d 100644 --- a/shell/src/session/sessionController.cpp +++ b/shell/src/session/sessionController.cpp @@ -2,7 +2,31 @@ SessionController::SessionController(QObject *parent) : QObject(parent) -{} +{ + connect(&authenticationBackend, + &AuthenticationBackend::authenticationFinished, + this, + [this](quint64 requestId, AuthenticationBackend::Result result) { + if (requestId != m_requestId || m_state != State::AUTHENTICATING) { + return; + } + + switch(result) { + case AuthenticationBackend::Result::SUCCESS: + setErrorMsg(""); + setState(State::UNLOCKED); + break; + case AuthenticationBackend::Result::REJECTED: + setErrorMsg("Authentication rejected"); + setState(State::LOCKED); + break; + case AuthenticationBackend::Result::BACKENDFAILURE: + setErrorMsg("Authentication unavailable"); + setState(State::LOCKED); + break; + } + }); +} QString SessionController::getErrorMsg() const { @@ -16,25 +40,26 @@ bool SessionController::isLocked() const void SessionController::lock() { + ++m_requestId; setState(State::LOCKED); } void SessionController::requestUnlock(const QString &password) { + if (m_state != State::LOCKED) { + return; + } + + setErrorMsg(""); + if (password.isEmpty()) { setErrorMsg("Password cannot be empty"); return; } - + + ++m_requestId; setState(State::AUTHENTICATING); - - // TODO Implement logic for checking password - if (password == "root") { - setState(State::UNLOCKED); - } else { - setState(State::LOCKED); - setErrorMsg("Incorrect Password"); - } + authenticationBackend.authenticate(m_requestId, password); } void SessionController::setState(const State state) diff --git a/shell/src/session/sessionController.h b/shell/src/session/sessionController.h index 672c1e4..37771e0 100644 --- a/shell/src/session/sessionController.h +++ b/shell/src/session/sessionController.h @@ -1,5 +1,6 @@ #pragma once +#include "PamAuthenticationBackend.h" #include #include #include @@ -30,6 +31,9 @@ class SessionController : public QObject void errorMsgChanged(); private: + PamAuthenticationBackend authenticationBackend; + quint64 m_requestId = 0; + State m_state = State::LOCKED; QString error_msg; From 2e4867c449ccd3418fddd8af6a32f85d3806c225 Mon Sep 17 00:00:00 2001 From: Sindre Date: Wed, 7 Oct 2026 16:15:17 +0200 Subject: [PATCH 09/13] Make AuthenticationBackend async Signed-off-by: Sindre --- shell/CMakeLists.txt | 4 +- shell/src/auth/PamAuthenticationBackend.cpp | 45 +++++++++++++-------- shell/src/auth/PamAuthenticationBackend.h | 6 ++- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index a4b2932..2d643fb 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -14,7 +14,7 @@ if(NOT PAM_INCLUDE_DIR OR NOT PAM_LIBRARY) message(FATAL_ERROR "PAM development files not found. Install libpam0g-dev on Ubuntu, or provide PAM in the target sysroot.") endif() -find_package(Qt6 6.5 REQUIRED COMPONENTS Quick QuickControls2) +find_package(Qt6 6.5 REQUIRED COMPONENTS Quick QuickControls2 Concurrent) qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(app src/main.cpp) @@ -39,5 +39,5 @@ qt_add_qml_module(app target_include_directories(app PRIVATE src/session src/auth ${PAM_INCLUDE_DIR}) -target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2 ${PAM_LIBRARY}) +target_link_libraries(app PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Concurrent ${PAM_LIBRARY}) target_compile_options(app PRIVATE -Wall -Wextra) diff --git a/shell/src/auth/PamAuthenticationBackend.cpp b/shell/src/auth/PamAuthenticationBackend.cpp index 2de0f1c..47ba2e0 100644 --- a/shell/src/auth/PamAuthenticationBackend.cpp +++ b/shell/src/auth/PamAuthenticationBackend.cpp @@ -6,8 +6,11 @@ #include #include #include +#include #include #include +#include +#include int PamAuthenticationBackend::conversationCallback( int messageCount, const pam_message **messages, pam_response **responses, void *appdata) @@ -68,16 +71,28 @@ int PamAuthenticationBackend::conversationCallback( return PAM_SUCCESS; } -//TODO make async void PamAuthenticationBackend::authenticate(quint64 request_id, QString password) { - QByteArray username; - try { - username = getCurrentUsername(); - } catch (const std::exception &e) { - emit authenticationFinished(request_id, Result::BACKENDFAILURE); - return; - } + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, request_id] { + const Result result = watcher->result(); + watcher->deleteLater(); + emit authenticationFinished(request_id, result); + }); + + // The worker owns its input and does not access the backend object. + watcher->setFuture(QtConcurrent::run([password = std::move(password)]() mutable { + try { + return authenticateBlocking(std::move(password)); + } catch (...) { + return Result::BACKENDFAILURE; + } + })); +} + +AuthenticationBackend::Result PamAuthenticationBackend::authenticateBlocking(QString password) +{ + const QByteArray username = getCurrentUsername(); struct ConversationContext context { @@ -89,27 +104,23 @@ void PamAuthenticationBackend::authenticate(quint64 request_id, QString password pam_handle_t *handle = nullptr; int status = pam_start("sudo", context.username.constData(), &conversation, &handle); - if (status != PAM_SUCCESS) { - emit authenticationFinished(request_id, Result::BACKENDFAILURE); - return; - } + if (status != PAM_SUCCESS) + return Result::BACKENDFAILURE; status = pam_authenticate(handle, PAM_DISALLOW_NULL_AUTHTOK); if (status != PAM_SUCCESS) { pam_end(handle, status); - emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); - return; + return resolveResultFromPamStatus(status); } status = pam_acct_mgmt(handle, 0); if (status != PAM_SUCCESS) { pam_end(handle, status); - emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); - return; + return resolveResultFromPamStatus(status); } status = pam_end(handle, status); - emit authenticationFinished(request_id, resolveResultFromPamStatus(status)); + return resolveResultFromPamStatus(status); } QByteArray PamAuthenticationBackend::getCurrentUsername() diff --git a/shell/src/auth/PamAuthenticationBackend.h b/shell/src/auth/PamAuthenticationBackend.h index efc7517..4a7a47a 100644 --- a/shell/src/auth/PamAuthenticationBackend.h +++ b/shell/src/auth/PamAuthenticationBackend.h @@ -25,9 +25,11 @@ class PamAuthenticationBackend : public AuthenticationBackend QByteArray password; }; - QByteArray getCurrentUsername(); + static Result authenticateBlocking(QString password); - Result resolveResultFromPamStatus(int status); + static QByteArray getCurrentUsername(); + + static Result resolveResultFromPamStatus(int status); static int conversationCallback( int messageCount, From ee1785bfa6e2f036d656e7af60d9807f931fbcfd Mon Sep 17 00:00:00 2001 From: Sindre Date: Mon, 19 Oct 2026 11:40:21 +0200 Subject: [PATCH 10/13] Set Systemd target to graphical for QEMU, and fix mosaic service Signed-off-by: Sindre --- docs/building.rst | 12 ++++++++++-- yocto/layers/meta-mosaic-product/README | 6 +++++- .../recipes-core/images/mosaic-image.bb | 1 + .../mosaic-session/files/mosaic-shell.service | 15 ++++++++++----- .../mosaic-session/mosaic-session_0.1.bb | 2 +- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/building.rst b/docs/building.rst index 45e7c56..550ee82 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -156,12 +156,20 @@ exporting artifacts. QEMU startup ---------------------- -``mosaic-shell.service`` starts ``/usr/bin/mosaic-shell`` on VT1 as the locked, +QEMU images boot to ``graphical.target``, which starts ``mosaic-shell.service``. +The service runs ``/usr/bin/mosaic-shell`` on VT1 as the locked, non-login ``mosaic`` account with ``video`` and ``input`` group membership. It waits for DRM card0, VT1, and systemd user-session initialization, and conflicts with -``getty@tty1.service``. Use the serial console for administration and diagnostics. +``getty@tty1.service``. If VT1 already has a controlling process, startup fails +instead of waiting for that process to release it. Use the serial console for +administration and diagnostics. systemd creates ``/run/mosaic`` with mode ``0700`` and sets ``XDG_RUNTIME_DIR`` to that directory. +It also creates ``/var/lib/mosaic`` with mode ``0750`` as ``HOME`` and +``/var/cache/mosaic`` with mode ``0700`` as ``XDG_CACHE_HOME``. These directories +are owned by ``mosaic`` and persist across service restarts and boots. +Output goes to journald, and failures restart after two seconds, with at most +five starts per minute. The product layer installs ``/etc/mosaic/session.env`` with: diff --git a/yocto/layers/meta-mosaic-product/README b/yocto/layers/meta-mosaic-product/README index cd21eaa..1cd56b5 100644 --- a/yocto/layers/meta-mosaic-product/README +++ b/yocto/layers/meta-mosaic-product/README @@ -13,7 +13,8 @@ Both qemu-dev and qemu-prod use 512 MiB RAM and the upstream virtio GPU, USB keyboard, and USB tablet configuration. STM32 profiles include the shell without this graphical startup package. -mosaic-session installs and enables mosaic-shell.service on multi-user.target. +mosaic-session installs and enables mosaic-shell.service on graphical.target. +The QEMU image boots to graphical.target in both dev and prod profiles. The service runs /usr/bin/mosaic-shell as the locked, non-login mosaic account, with video and input group membership. A udev rule grants that account exclusive access to tty1 and grants the video group read/write access to DRM card0. @@ -21,7 +22,10 @@ Input access uses the standard udev input group permissions. The service waits for card0, tty1, and systemd-user-sessions.service. It owns VT1 and conflicts with getty@tty1.service. The serial console remains available. +If another process already controls VT1, startup fails instead of waiting for it. systemd creates /run/mosaic with mode 0700 and sets XDG_RUNTIME_DIR to that path. +It also creates /var/lib/mosaic with mode 0750 as HOME and /var/cache/mosaic +with mode 0700 as XDG_CACHE_HOME. Both persist across service restarts and boots. /etc/mosaic/session.env selects Qt LinuxFB with DRM dumb buffers and software Qt Quick rendering. The session depends on shell, Qt platform/QML plugins, diff --git a/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb b/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb index c37aa4d..74b13ba 100644 --- a/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb +++ b/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb @@ -7,5 +7,6 @@ DESCRIPTION = "Machine-independent Mosaic image with development and production IMAGE_BASENAME = "mosaic-image-${MOSAIC_BUILD_PROFILE}" IMAGE_INSTALL:append = " packagegroup-mosaic packagegroup-mosaic-apps" +SYSTEMD_DEFAULT_TARGET:qemuarm64 = "graphical.target" IMAGE_FEATURES:append:mosaic-dev = " tools-debug dbg-pkgs" IMAGE_FEATURES:remove:mosaic-prod = " tools-debug dbg-pkgs" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service index 8df6180..1381b91 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service @@ -1,10 +1,9 @@ [Unit] Description=Mosaic graphical shell -Requires=dev-dri-card0.device dev-tty1.device -Wants=systemd-user-sessions.service +Requires=dev-dri-card0.device dev-tty1.device systemd-user-sessions.service After=dev-dri-card0.device dev-tty1.device systemd-user-sessions.service Conflicts=getty@tty1.service -Before=getty@tty1.service +Before=getty@tty1.service graphical.target StartLimitIntervalSec=60 StartLimitBurst=5 @@ -13,13 +12,19 @@ Type=simple User=mosaic Group=mosaic SupplementaryGroups=video input +StateDirectory=mosaic +StateDirectoryMode=0750 +CacheDirectory=mosaic +CacheDirectoryMode=0700 RuntimeDirectory=mosaic RuntimeDirectoryMode=0700 Environment=XDG_RUNTIME_DIR=/run/mosaic +Environment=HOME=/var/lib/mosaic +Environment=XDG_CACHE_HOME=/var/cache/mosaic EnvironmentFile=/etc/mosaic/session.env ExecStart=/usr/bin/mosaic-shell TTYPath=/dev/tty1 -StandardInput=tty +StandardInput=tty-fail TTYReset=yes TTYVHangup=yes TTYVTDisallocate=yes @@ -32,4 +37,4 @@ CapabilityBoundingSet= UMask=0077 [Install] -WantedBy=multi-user.target +WantedBy=graphical.target diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb index 8eb71f5..e193f00 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb @@ -17,7 +17,7 @@ RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins ttf-dejavu-sans" USERADD_PACKAGES = "${PN}" GROUPADD_PARAM:${PN} = "--system mosaic" -USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input --home-dir / \ +USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input --home-dir /var/lib/mosaic \ --no-create-home --shell ${base_sbindir}/nologin --password '!' mosaic" SYSTEMD_SERVICE:${PN} = "mosaic-shell.service" From c7255a327a020da0b2bb9028fba5f1e4cf6b0a3f Mon Sep 17 00:00:00 2001 From: Sindre Date: Mon, 19 Oct 2026 15:06:25 +0200 Subject: [PATCH 11/13] Configure graphical dependencies for qemu image, and add PAM for login authentication Signed-off-by: Sindre --- docs/building.rst | 38 +++++++++++++------ yocto/layers/meta-mosaic-product/README | 19 +++++++--- .../conf/distro/mosaic.conf | 5 ++- .../files/70-mosaic-session.rules | 1 + .../mosaic-session/files/eglfs_kms.json | 4 ++ .../mosaic-session/files/mosaic-shell.service | 2 +- .../mosaic-session/files/session.env | 9 +++-- .../mosaic-session/mosaic-session_0.1.bb | 10 +++-- yocto/layers/meta-mosaic/README | 6 ++- .../recipes-shell/shell/shell_0.1.bb | 4 +- 10 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json diff --git a/docs/building.rst b/docs/building.rst index 550ee82..c1bc056 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -158,7 +158,7 @@ QEMU startup QEMU images boot to ``graphical.target``, which starts ``mosaic-shell.service``. The service runs ``/usr/bin/mosaic-shell`` on VT1 as the locked, -non-login ``mosaic`` account with ``video`` and ``input`` group membership. It waits +non-login ``mosaic`` account with ``video``, ``input``, and ``render`` group membership. It waits for DRM card0, VT1, and systemd user-session initialization, and conflicts with ``getty@tty1.service``. If VT1 already has a controlling process, startup fails instead of waiting for that process to release it. Use the serial console for @@ -175,16 +175,32 @@ The product layer installs ``/etc/mosaic/session.env`` with: .. code-block:: text - QT_QPA_PLATFORM=linuxfb - QT_QPA_FB_DRM=1 - QT_QUICK_BACKEND=software - -These settings select LinuxFB using DRM dumb buffers and software rendering. -The session includes Qt platform/QML plugins and DejaVu Sans. It uses the pinned -Qt recipe's ``linuxfb``, ``kms``, ``libinput``, and ``udev`` support. Product graphics -policy belongs in ``meta-mosaic-product``, including the service, environment file, -and udev rules under ``recipes-shell/mosaic-session/``. To change the environment -on a running image, edit ``/etc/mosaic/session.env`` and restart the service. + QT_QPA_PLATFORM=eglfs + QT_QPA_EGLFS_INTEGRATION=eglfs_kms + QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json + QT_QPA_EGLFS_HIDECURSOR=1 + QSG_RHI_BACKEND=opengl + QT_SHADER_CACHE_PATH=/var/cache/mosaic + +These settings select EGLFS/KMS and Qt Quick's OpenGL renderer. The installed +``/etc/mosaic/eglfs_kms.json`` selects ``/dev/dri/card0`` and disables the hardware +cursor. The session hides the pointer and stores shader cache files in its +persistent cache directory. Udev rules grant the ``video`` group access to card0 +and the ``render`` group access to DRM render nodes. + +The QEMU distro override enables ``opengl``. The pinned Qt recipe then enables +``gbm``, ``gles2``, and ``eglfs`` alongside ``kms``, ``libinput``, and ``udev``; +Mesa provides EGL, GLES, GBM, and runtime drivers. The session includes Mesa's +runtime drivers explicitly because they are loaded dynamically, along with Qt +platform/QML plugins and DejaVu Sans. The standard virtio GPU uses Mesa's +software rendering fallback, so host GPU acceleration is not required. +This uses the OpenGL rendering pipeline rather than Qt Quick's software backend. +STM32 graphics configuration is unchanged. + +Product graphics policy belongs in ``meta-mosaic-product``, including the service, +environment file, KMS configuration, and udev rules under +``recipes-shell/mosaic-session/``. To change the display setup on a running image, +edit ``/etc/mosaic/session.env`` or ``/etc/mosaic/eglfs_kms.json`` and restart the service. A missing environment file or failed display/QML startup fails the service. Add another machine diff --git a/yocto/layers/meta-mosaic-product/README b/yocto/layers/meta-mosaic-product/README index 1cd56b5..a34d196 100644 --- a/yocto/layers/meta-mosaic-product/README +++ b/yocto/layers/meta-mosaic-product/README @@ -16,8 +16,9 @@ without this graphical startup package. mosaic-session installs and enables mosaic-shell.service on graphical.target. The QEMU image boots to graphical.target in both dev and prod profiles. The service runs /usr/bin/mosaic-shell as the locked, non-login mosaic account, -with video and input group membership. A udev rule grants that account exclusive -access to tty1 and grants the video group read/write access to DRM card0. +with video, input, and render group membership. A udev rule grants that account +exclusive access to tty1 and grants the video group read/write access to DRM card0. +DRM render nodes are accessible to the render group. Input access uses the standard udev input group permissions. The service waits for card0, tty1, and systemd-user-sessions.service. It owns VT1 @@ -27,10 +28,16 @@ systemd creates /run/mosaic with mode 0700 and sets XDG_RUNTIME_DIR to that path It also creates /var/lib/mosaic with mode 0750 as HOME and /var/cache/mosaic with mode 0700 as XDG_CACHE_HOME. Both persist across service restarts and boots. -/etc/mosaic/session.env selects Qt LinuxFB with DRM dumb buffers and software -Qt Quick rendering. The session depends on shell, Qt platform/QML plugins, -and DejaVu Sans. The pinned qtbase recipe enables linuxfb, kms, libinput, and -udev with the current distro configuration. No compositor is selected. +/etc/mosaic/session.env selects Qt EGLFS with the eglfs_kms integration and +the OpenGL Qt Quick renderer. /etc/mosaic/eglfs_kms.json selects DRM card0 and +disables the hardware cursor; the session hides the pointer entirely. Shader +cache files use /var/cache/mosaic. + +The QEMU distro override enables opengl, which selects EGL, GLES, GBM, and EGLFS +in the pinned Qt/Mesa recipes. The session includes Mesa's runtime drivers, +Qt platform/QML plugins, and DejaVu Sans. The standard virtio GPU uses Mesa's +software rendering fallback; host GPU acceleration is not required. STM32 +graphics policy is unchanged. Mosaic owns the display directly through EGLFS. Output goes to journald. Failed processes restart after two seconds, with at most five starts in a 60-second interval. After correcting a persistent failure, use diff --git a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf index db61bf5..d0dfa9f 100644 --- a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf +++ b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf @@ -20,7 +20,10 @@ FULL_OPTIMIZATION = "-Os ${DEBUG_LEVELFLAG}" # explicit feature baseline DISTRO_FEATURES_OPTED_OUT = "*" -DISTRO_FEATURES = "pci ldconfig ptest multiarch ipv4 ipv6 wayland" +DISTRO_FEATURES = "pci ldconfig pam ptest multiarch ipv4 ipv6 wayland" + +# The QEMU session uses Qt EGLFS/KMS with Mesa EGL, GLES, and GBM. +DISTRO_FEATURES:append:qemuarm64 = " opengl" PACKAGECONFIG:remove:pn-systemd = "hibernate localed machined nss-mymachines" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules index 8619c07..9fc48be 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules @@ -1,2 +1,3 @@ SUBSYSTEM=="tty", KERNEL=="tty1", OWNER="mosaic", GROUP="mosaic", MODE="0600", TAG+="systemd" SUBSYSTEM=="drm", KERNEL=="card0", GROUP="video", MODE="0660", TAG+="systemd" +SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="render", MODE="0660" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json new file mode 100644 index 0000000..db7a503 --- /dev/null +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json @@ -0,0 +1,4 @@ +{ + "device": "/dev/dri/card0", + "hwcursor": false +} diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service index 1381b91..77e860d 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service @@ -11,7 +11,7 @@ StartLimitBurst=5 Type=simple User=mosaic Group=mosaic -SupplementaryGroups=video input +SupplementaryGroups=video input render StateDirectory=mosaic StateDirectoryMode=0750 CacheDirectory=mosaic diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env index 06ae42e..de5a5cc 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env @@ -1,3 +1,6 @@ -QT_QPA_PLATFORM=linuxfb -QT_QPA_FB_DRM=1 -QT_QUICK_BACKEND=software +QT_QPA_PLATFORM=eglfs +QT_QPA_EGLFS_INTEGRATION=eglfs_kms +QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json +QT_QPA_EGLFS_HIDECURSOR=1 +QSG_RHI_BACKEND=opengl +QT_SHADER_CACHE_PATH=/var/cache/mosaic diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb index e193f00..4b28ad8 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb @@ -5,6 +5,7 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec SRC_URI = " \ file://mosaic-shell.service \ file://session.env \ + file://eglfs_kms.json \ file://70-mosaic-session.rules \ " S = "${UNPACKDIR}" @@ -13,11 +14,11 @@ COMPATIBLE_MACHINE = "^qemuarm64$" inherit systemd useradd -RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins ttf-dejavu-sans" +RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins mesa-megadriver ttf-dejavu-sans" USERADD_PACKAGES = "${PN}" -GROUPADD_PARAM:${PN} = "--system mosaic" -USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input --home-dir /var/lib/mosaic \ +GROUPADD_PARAM:${PN} = "--system mosaic; --system render" +USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input,render --home-dir /var/lib/mosaic \ --no-create-home --shell ${base_sbindir}/nologin --password '!' mosaic" SYSTEMD_SERVICE:${PN} = "mosaic-shell.service" @@ -27,8 +28,9 @@ do_install() { install -d ${D}${systemd_system_unitdir} ${D}${sysconfdir}/mosaic ${D}${nonarch_libdir}/udev/rules.d install -m 0644 ${UNPACKDIR}/mosaic-shell.service ${D}${systemd_system_unitdir}/mosaic-shell.service install -m 0644 ${UNPACKDIR}/session.env ${D}${sysconfdir}/mosaic/session.env + install -m 0644 ${UNPACKDIR}/eglfs_kms.json ${D}${sysconfdir}/mosaic/eglfs_kms.json install -m 0644 ${UNPACKDIR}/70-mosaic-session.rules ${D}${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules } -CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env" +CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env ${sysconfdir}/mosaic/eglfs_kms.json" FILES:${PN} += "${systemd_system_unitdir}/mosaic-shell.service ${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules" diff --git a/yocto/layers/meta-mosaic/README b/yocto/layers/meta-mosaic/README index 991dcbd..849e90a 100644 --- a/yocto/layers/meta-mosaic/README +++ b/yocto/layers/meta-mosaic/README @@ -38,8 +38,10 @@ Layer contents ============== shell: Builds the local shell/ sources with Qt 6 and installs the - executable as /usr/bin/mosaic-shell. The recipe declares Qt - platform plugins and Qt Quick QML imports as runtime dependencies. + executable as /usr/bin/mosaic-shell. The recipe depends on libpam + for the lock screen's authentication backend. Runtime dependencies + include Qt platform plugins, Qt Quick QML imports, and libpam-runtime + for PAM's common configuration, authentication modules, and helper. Build the package for a machine and profile: diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb index ca1770f..cd8b2d5 100644 --- a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb @@ -15,9 +15,9 @@ S = "${UNPACKDIR}/shell" inherit qt6-cmake -DEPENDS += "qtbase qtdeclarative qtdeclarative-native" +DEPENDS += "qtbase qtdeclarative qtdeclarative-native libpam" -RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins" +RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins libpam-runtime" do_compile() { case "${MOSAIC_APP_LIFECYCLE}" in From 5e1fa1a29542dcc767dffe1e0c1086fc1440e416 Mon Sep 17 00:00:00 2001 From: Sindre Date: Mon, 19 Oct 2026 15:13:00 +0200 Subject: [PATCH 12/13] Create mosaic-shell.pam service Signed-off-by: Sindre --- docs/building.rst | 16 ++++++++++++++++ yocto/layers/meta-mosaic/README | 3 +++ .../recipes-shell/shell/files/mosaic-shell.pam | 4 ++++ .../meta-mosaic/recipes-shell/shell/shell_0.1.bb | 6 +++++- 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam diff --git a/docs/building.rst b/docs/building.rst index c1bc056..d013f21 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -203,6 +203,22 @@ environment file, KMS configuration, and udev rules under edit ``/etc/mosaic/session.env`` or ``/etc/mosaic/eglfs_kms.json`` and restart the service. A missing environment file or failed display/QML startup fails the service. +Lock-screen authentication +-------------------------- + +The shell package installs ``/etc/pam.d/mosaic-shell``. The authentication backend +selects this PAM service to check the current process user's password and account +status using the system's ``common-auth`` and ``common-account`` stacks, like sudo. +The backend rejects empty passwords and does not open a new login session or +change passwords. ``libpam-runtime`` supplies the common policies, authentication +modules, and password-checking helper. Local edits to the Mosaic policy are +preserved during package upgrades. + +Under ``mosaic-shell.service``, the authenticated user is ``mosaic``. The current +boot configuration still locks that account's password and sets +``NoNewPrivileges=yes``. Password provisioning and permission for PAM's +``unix_chkpwd`` helper must be configured before unlocking can work in the image. + Add another machine ------------------- diff --git a/yocto/layers/meta-mosaic/README b/yocto/layers/meta-mosaic/README index 849e90a..55bfa89 100644 --- a/yocto/layers/meta-mosaic/README +++ b/yocto/layers/meta-mosaic/README @@ -42,6 +42,9 @@ Layer contents for the lock screen's authentication backend. Runtime dependencies include Qt platform plugins, Qt Quick QML imports, and libpam-runtime for PAM's common configuration, authentication modules, and helper. + It installs /etc/pam.d/mosaic-shell and marks it as package configuration. + The backend uses this policy's common-auth and common-account stacks + to authenticate the current process user. Build the package for a machine and profile: diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam b/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam new file mode 100644 index 0000000..db7e24b --- /dev/null +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam @@ -0,0 +1,4 @@ +#%PAM-1.0 +# Unlock the current user's Mosaic session using the system authentication policy. +auth include common-auth +account include common-account diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb index cd8b2d5..abc96da 100644 --- a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb @@ -10,6 +10,7 @@ SRC_URI = " \ file://shell/src \ file://shell/qml \ file://LICENSE \ + file://mosaic-shell.pam \ " S = "${UNPACKDIR}/shell" @@ -19,6 +20,8 @@ DEPENDS += "qtbase qtdeclarative qtdeclarative-native libpam" RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins libpam-runtime" +CONFFILES:${PN} += "${sysconfdir}/pam.d/mosaic-shell" + do_compile() { case "${MOSAIC_APP_LIFECYCLE}" in terminate) @@ -38,6 +41,7 @@ do_compile() { } do_install() { - install -d ${D}${bindir} + install -d ${D}${bindir} ${D}${sysconfdir}/pam.d install -m 0755 ${B}/app ${D}${bindir}/mosaic-shell + install -m 0644 ${UNPACKDIR}/mosaic-shell.pam ${D}${sysconfdir}/pam.d/mosaic-shell } From 736c125d5d26678c4fde081e8902f18cf504ed10 Mon Sep 17 00:00:00 2001 From: Sindre Date: Mon, 19 Oct 2026 15:13:30 +0200 Subject: [PATCH 13/13] Refactor AuthenticationBackend to use the mosaic-shell service Signed-off-by: Sindre --- shell/src/auth/PamAuthenticationBackend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/src/auth/PamAuthenticationBackend.cpp b/shell/src/auth/PamAuthenticationBackend.cpp index 47ba2e0..b386393 100644 --- a/shell/src/auth/PamAuthenticationBackend.cpp +++ b/shell/src/auth/PamAuthenticationBackend.cpp @@ -103,7 +103,7 @@ AuthenticationBackend::Result PamAuthenticationBackend::authenticateBlocking(QSt pam_handle_t *handle = nullptr; - int status = pam_start("sudo", context.username.constData(), &conversation, &handle); + int status = pam_start("mosaic-shell", context.username.constData(), &conversation, &handle); if (status != PAM_SUCCESS) return Result::BACKENDFAILURE;