Releases: presire/qSnapper
qSnapper 1.3.4
Release v1.3.4: harden live-path restore operations with dirfd-based helpers
This change completes the v1.3.4 follow-up for the remaining restore-path defense-in-depth
issue discussed during the SUSE security review.
The core goal is to remove the last live-filesystem mutation paths that still depended
on absolute-path syscalls in restoreFilesImpl() and copySymlink(),
and to replace them with helper APIs anchored to trusted parent directory file descriptors.
Background
The earlier v1.3.3 security hardening already moved regular-file restore operations onto
openat(..., O_NOFOLLOW) and fd-based metadata updates.
However, several live-path operations still relied on path-based rename/open/lchown/utimensat calls.
Those remaining operations were not considered immediately exploitable in the default deployment model,
but they still left a defense-in-depth concern when restoring into parent directories writable by non-root
service users.
This commit closes that gap by extending the filesystem helper layer and migrating the remaining live-path
restore code to it.
Main changes
-
Add dirfd-based live-path helpers in filesystemhelpers
- Add safeOpenDirectory() for O_NOFOLLOW directory opens via a trusted parent walk.
- Add safeRenamePathNoFollow() using renameat() on source/destination parent dirfds.
- Add safeReadLinkNoFollow() using readlinkat() from a trusted parent dirfd.
- Add safeCreateSymlinkNoFollow() using symlinkat() from a trusted parent dirfd.
- Add safeSetSymlinkMetadataNoFollow() using fchownat(...,
AT_SYMLINK_NOFOLLOW) and utimensat(..., AT_SYMLINK_NOFOLLOW).
-
Migrate remaining live-path restore operations to the new helpers
- Replace movePathAsideNoFollow() path-based rename() with safeRenamePathNoFollow().
- Replace directory restore open(systemFilePath, ... O_DIRECTORY | O_NOFOLLOW) with safeOpenDirectory().
- Rework copySymlink() to use safeReadLinkNoFollow(),
safeCreateSymlinkNoFollow(), safeRenamePathNoFollow(), and
safeSetSymlinkMetadataNoFollow() instead of path-based readlink(),
symlink(), rename(), lchown(), and utimensat().
-
Preserve and clarify trust boundaries
- Keep safeLstat() explicitly documented as a read-only helper for
trusted snapshot parents and display-only metadata collection. - Clarify in comments that snapshot-side reads remain path-based by
design, while live-path mutations are now dirfd-based.
- Keep safeLstat() explicitly documented as a read-only helper for
-
Finish the remaining correctness cleanup from the helper migration
- Make safeReadLinkNoFollow() retry with a growing QByteArray instead
of a fixed PATH_MAX buffer so large symlink targets are read without
silent truncation.
- Make safeReadLinkNoFollow() retry with a growing QByteArray instead
-
Extend unit coverage
- Add tests for safeOpenDirectory() rejecting symlink paths.
- Add tests for safeRenamePathNoFollow() under trusted and symlinked parent scenarios.
- Add tests for safeCreateSymlinkNoFollow(), safeReadLinkNoFollow(), and safeSetSymlinkMetadataNoFollow().
- Add a regression test that verifies large symlink targets round-trip without truncation.
Other low-risk hardening included in this work
- In fssnapshotstore.cpp, remove the open-fail -> QFile::exists()
pattern and simplify failure handling to QFile::errorString(). - In copyRegularFile(), fail explicitly if sendfile() returns 0 before
the expected byte count is copied, preventing a potential infinite
loop on unexpected EOF. - Document the /tmp fallback in SingleInstanceGuard as a GUI-side
graceful degradation path, not a privileged security boundary.
Verification
- Debug build: qsnapper-dbus-service rebuilt successfully
- Release build: qsnapper-dbus-service rebuilt successfully
- Unit tests passed:
- tst_configname
- tst_filepaths
- tst_filesystemhelpers
Result
After this change, live-path restore mutations no longer depend on path-based rename/open/symlink-metadata syscalls.
The remaining restore path now consistently uses trusted parent dirfd traversal for file and
symlink creation, rename, and metadata updates,
aligning the implementation with the v1.3.4 defense-in-depth objective raised in the SUSE review.
What's Changed
New Contributors
Full Changelog: v1.3.3...v1.3.4
qSnapper 1.3.3
Release v1.3.3: Security hardening (CVE-2026-41045..CVE-2026-41049)
This release addresses five CVE-class security issues identified during an external security review
by the SUSE security team, conducted under coordinated disclosure (CRD: 2026-05-26).
The fixes are delivered as four logical commits (P0 / P1 / P2 / Round 2) that harden the D-Bus service,
input validation, authorization, and logging layers.
CVE summary
- CVE-2026-41045: Polkit authentication bypass via misuse of UnixProcessSubject
- CVE-2026-41046: Path traversal via configName leading to local DoS and information disclosure
- CVE-2026-41047: Information disclosure through diff/list operations reachable without admin authentication
- CVE-2026-41048: Authentication bypass across D-Bus clients via shared m_authenticated flag
- CVE-2026-41049: Authentication bypass by implicit carry-over between different Polkit actions
Major changes
P0 - Immediately block local root escalation paths
- Replace UnixProcessSubject with SystemBusNameSubject in Polkit checkAuthorization(), closing the PID-reuse race.
- Remove the m_authenticated cache and the Authenticate() D-Bus method entirely;
rely on Polkit's native auth_admin_keep behaviour instead. - Add strict validateConfigName() (allowlist: ^[A-Za-z0-9_.-]+$) and apply it at the entry of every D-Bus method
that accepts configName. - Merge RestoreFiles / RestoreFilesDirect into a single hardened restoreFilesImpl():
require absolute paths, reject "..", use openat() + O_NOFOLLOW, and verify containment within the snapshot
root via weakly_canonical.
P1 - Split diff authorization, remove Quit D-Bus method, harden log permissions
- Introduce new Polkit action com.presire.qsnapper.view-diff with auth_admin_keep;
move GetFileChanges, GetFileChangesBetween, GetFileDiffBetween, and GetFileDiffAndDetails under it.
list-snapshots now only authorises pure snapshot enumeration. - Remove the Quit() D-Bus method (no auth check, DoS vector); service shutdown now relies solely on the existing 5-minute idle timer.
- Harden /var/log/qsnapper permissions: directory 0700, log files 0600, enforced via both a systemd-tmpfiles drop-in and runtime
setPermissions().
P2 - Enforce per-member D-Bus ACL
- Replace the permissive "allow everything to our destination" D-Bus
policy with a deny-by-default per-member ACL that re-allows only the 15 declared public methods plus Introspectable and Peer. - org.freedesktop.DBus.Properties is intentionally denied because the service exposes no D-Bus properties.
P3 - Address review comments (SUSE round-2 feedback)
- Extend containsDangerousChar() to reject all Unicode control ranges:
C0 (U+0000..U+001F), DEL (U+007F), and C1 (U+0080..U+009F). - Remove the redundant containsDangerousChar() call from validateConfigName() since the allowlist regex already subsumes it.
- Rewrite isPathWithinSnapshotRoot() as a direct string-prefix comparison with '/' boundary check,
replacing the lexically_relative()-based approach and preventing the sibling-root trick. - Rename validateConfigOrFail() -> resolveConfigOrFail() and fold the "empty -> root" default into it,
eliminating the inconsistent "!configName.isEmpty() && validateConfigOrFail(...)" boilerplate at every callsite. - Demote restore operation logs from qWarning to qInfo (systemd journal priority 6).
- Move restoreProgress emission past per-element input validation so
the D-Bus progress signal cannot carry unaccepted caller paths. - Update inline documentation and Doxygen comments.
License change
Realign project license from GPL-3.0 to GPL-2.0-or-later to match libbtrfsutil (LGPL-2.1) which qSnapper links against.
License headers and packaging metadata are updated accordingly.
This change is independent of the security review and has no security implications.
Known limitations (documented for v1.3.4 follow-up)
The TOCTOU concern in restoreFilesImpl() / copyRegularFile() (chown / file creation against service-user-controlled parent directories)
is tracked for v1.3.4 to be addressed openly after the coordinated release.
This is a defense-in-depth issue that only matters if a service account is already compromised.
Full Changelog: v1.3.2...v1.3.3
qSnapper 1.3.2
Updated to version 1.3.2
Add version guard for snapper ConfigInfo API compatibility
Use preprocessor version check to handle the renamed method in libsnapper 6.0+ (snapper 0.10.1+).
Older versions use getConfigName() while 6.0 (before snapper 0.10.0) and later use get_config_name().
Full Changelog: v1.3.1...v1.3.2
qSnapper 1.3.1
Updated to version 1.3.1
Replace external command invocations with library/API calls
Replace all QProcess-based external command calls (except installation-helper) with direct library, POSIX,
and D-Bus calls, eliminating runtime dependencies on system utilities.
Simple replacements:
- /usr/bin/snapper (list-configs): Snapper::getConfigs("/")
- /usr/sbin/btrfs: libbtrfsutil
- /bin/mkdir: std::filesystem::create_directories()
- /bin/stat + /bin/chmod: POSIX lstat() + chmod()
- /bin/chown: POSIX chown()
File copy/remove:
- /bin/rm -rf: std::filesystem::remove_all()
- /bin/cp: custom copyRegularFile() (FICLONE + sendfile fallback) and copySymlink(),
preserving ownership and timestamps
D-Bus method migration:
- Add IsConfigured(), WriteSnapperConfig(), SetupQuota() on service side
- Migrate client-side snapper commands to D-Bus calls
- Add D-Bus XML interface and Polkit policy for configure action
In-process diff:
- /usr/bin/diff -u: Myers diff algorithm with unified output format
- Replace #include with and
Full Changelog: v1.3.0...v1.3.1
qSnapper 1.3.0
Updated to version 1.3.0
Extend qSnapper beyond a single fixed Snapper config and add two long-requested workflows:
editing existing snapshot metadata and comparing two arbitrary snapshots.
Multi-config support
- Add ListConfigs D-Bus method and plumb a configName argument through every snapshot operation
(List/Create/Modify/Delete/Rollback, GetFileChanges*, GetFileDiff*). - Expose
configsandcurrentConfigproperties on SnapperService so the UI can switch between configured Snapper configs at runtime. - Cache the config list and reconnect to the D-Bus service on transient failures.
(SnapperService::reconnect, FileChangeModel::reconnectDbus)
Snapshot editing
- New ModifySnapshot D-Bus method and matching SnapperService / SnapshotListModel API to update description,
cleanup algorithm and userdata on an existing snapshot. - New polkit action com.presire.qsnapper.modify-snapshot (EN/JA/DE) and a SnapshotEditDialog.qml front-end wired into SnapshotListPage.
- Pass userdata as QMap<QString,QString> over D-Bus (a{ss});
register the metatype in both the GUI and the servicemain.cpp,
and thread userdata through createSingle/createPre/createPost.
Compare two snapshots
- New GetFileChangesBetween and GetFileDiffBetween D-Bus methods.
- FileChangeModel gains a "between" mode plus a flat (non-tree) presentation mode for list-style rendering.
- New CompareSnapshotsDialog.qml exposes the workflow from the snapshot list page.
SELinux policy
- Rewrite selinux/qsnapper.te: drop unused interfaces and redundant allow rules, consolidate file/exec/dbus permissions.
Net -289/+165 lines while keeping the same runtime capabilities.
Build / packaging
- Default CMAKE_INSTALL_PREFIX to /usr when unset.
- Register the new QML components (SnapshotEditDialog, CompareSnapshotsDialog) with qt6_add_qml_module.
- Set QTP0004 to OLD to silence qmldir warnings for resource-embedded subdirectories,
and add NO_IMPORT_SCAN.
(shared-Qt builds finalize automatically; the explicit qt_import_qml_plugins / qt_finalize_executable calls
produced spurious link warnings and have been removed — re-enable them for static Qt builds)
Misc
- Update German and Japanese translations for the new edit/compare
strings.
- Cosmetic alignment cleanup in ThemeManager color palette constants.
Full Changelog: v1.2.3...v1.3.0
qSnapper 1.2.3
Updated to version 1.2.3
The "System Rollback" button in the snapshot detail dialog previously called Snapshot::setDefault() directly on the selected snapshot,
which is only correct for transactional-update systems (read-only root).
On a standard openSUSE system with a writable root subvolume, this caused the read-only target snapshot itself to become the default subvolume,
leaving the system unable to boot into a writable state.
Reimplement SnapshotOperations::RollbackSnapshot() to mirror the logic of snapper rollback N
(client/snapper/cmd-rollback.cc):
-
Detect the ambit by inspecting whether the previous default snapshot is read-only.
-
CLASSIC ambit (writable previous default):
- Create a read-only backup snapshot of the current state
with description "rollback backup of #",
cleanup="number", userdata["important"]="yes". - Create a writable copy of the target snapshot with
description "writable copy of #". - If the previous default had an empty cleanup algorithm,
set it to "number" via modifySnapshot(). - Call setDefault() on the newly created writable copy.
- Create a read-only backup snapshot of the current state
-
TRANSACTIONAL ambit (read-only previous default):
Call setDefault() on the target snapshot directly, matching
the previous behavior for this case.
Both libsnapper >= 7.4 (with Plugins::Report) and older versions are supported via the existing LIBSNAPPER_VERSION_AT_LEAST macro,
and logPluginReport() is invoked after each plugin-aware call.
The GUI side (SnapshotListModel, SnapperService, QML) and the D-Bus interface signature (RollbackSnapshot(int)) are unchanged,
so no client-side changes or polkit action updates are required.
SELinux policy updates (selinux/qsnapper.te):
-
Allow qsnapper_dbus_t to execute snapper_grub_plugin_exec_t, required by libsnapper to invoke /usr/lib/snapper/plugins/grub
during setDefault()/rollback for bootloader entry updates. -
Allow qsnapper_t to read passwd_file_t and access systemd_userdbd_runtime_t for user name resolution during GUI
startup (pre-existing denials surfaced during rollback testing).
Full Changelog: v1.2.2...v1.2.3
qSnapper 1.2.2
Updated to version 1.2.2
Use multi-resolution application icons
Register qSnapper@64/@128/@256.png as Qt resources and install all sizes (64/128/256/512)
into the hicolor icon theme so desktop environments can pick the most appropriate resolution.
- main.cpp: build QIcon with addFile() for 64/128/256 variants so Qt selects
the best size for window decorations and taskbars, including HiDPI displays. - AboutqSnapperDialog.qml: switch the logo to qSnapper@256.png to stay crisp at the 200px display width on HiDPI screens.
- CMakeLists.txt: install each size under the matching hicolor apps directory and expose the new resources to the QRC.
Full Changelog: v1.2.1...v1.2.2
qSnapper 1.2.1
Updated to version 1.2.1
Add pre-authentication and fix file restore safety issues, bump to v1.2.1
- Add Authenticate() D-Bus method to perform Polkit auth once before batch restore operations,
avoiding repeated password prompts for each file in RestoreFiles/RestoreFilesDirect. - Preserve symlinks correctly by checking isSymLink() before isDir(),
since QFileInfo::isDir() follows symlinks and would misroute symlink-to-directory entries into the mkdir branch. - Replace
cp -awithcp -d --preserve=all --no-preserve=xattr
to prevent the btrfs read-onlyroxattr from propagating from snapshots to the root subvolume,
which could leave/mounted read-only. - Add a safety net that checks the root subvolume's
roproperty after
restore and resets it tofalseif it became read-only. - Skip dangerous paths under
/.snapshots/during restore to avoid corrupting snapshot metadata. - In RestoreFilesDirect, only
rmthe destination fortypechangedentries instead of unconditionally,
and bail out on rm failure.
Full Changelog: v1.2.0...v1.2.1
qSnapper 1.2.0
Updated to version 1.2.0
Rewrite file restore to use direct snapshot mount instead of Comparison::doUndoStep, bump to v1.2.0
Replace the previous restore mechanism based on Snapper's Comparison::doUndoStep() with
two new methods that directly mount the snapshot filesystem and copy files:
- "YaST compatible" mode: mirrors YaST's restore behavior using cp -a, rm -rf, chown, and chmod to restore files with proper ownership and permissions.
- "Direct copy" mode: same approach but uses cp --reflink=auto to leverage btrfs copy-on-write for faster restores on btrfs filesystems.
Both D-Bus methods (RestoreFiles, RestoreFilesDirect) now receive changeTypes alongside filePaths, enabling change-type-aware restore logic.
(e.g., removing files that were created after the snapshot, recreating deleted files)
Additional changes:
- Add restore options UI (method selection and batch size) to the confirm restore dialog, with settings persisted via QSettings.
- Add a scrollable restore log to the progress dialog showing each restored file path in real time.
- Replace hardcoded version strings with QSNAPPER_VERSION compile definition sourced from CMake's PROJECT_VERSION.
- Remove unused methods: dumpTree(), executeCommand(), getDBusInterface().
Full Changelog: v1.1.4...v1.2.0
qSnapper 1.1.4
Updated to version 1.1.4
Add complete runtime dependencies for RPM/DEB packages
QML runtime modules (QtQuick, QtQuick.Controls, QtQuick.Layouts) and PolicyKit are not detected by RPM AUTOREQ or DEB SHLIBDEPS
because they are dynamically loaded at runtime, not linked as shared libraries.
Without these explicit dependencies, installing via zypper/apt would
succeed but the application would fail to start with "module QtQuick.Controls is not installed" errors.
- Detect distro from /etc/os-release to set correct package names
- openSUSE/SLE: qt6-declarative-imports, qt6-quickcontrols2-imports, polkit
- Fedora: qt6-qtdeclarative, qt6-qtquickcontrols2, polkit
- Debian: qml6-module-qtquick, qml6-module-qtquick-controls, qml6-module-qtquick-layouts, polkitd | policykit-1
- Keep AUTOREQ/SHLIBDEPS ON for C++ shared library auto-detection
Full Changelog: v1.1.3...v1.1.4