[Bug]: KDE helper authorization fails when the XDG data path contains a percent sign #35972
Send to my agentSummary#
KDE SnapShot setup cannot authorize its installed helper when the data directory contains a percent sign on Plasma releases that enforce desktop-entry permissions. Reinstalling writes the same entry and leaves capture unavailable. A focused fixture reproduced the executable-path mismatch using the checked-out encoder and Qt's real parser; the resulting authorization failure is established from the tagged KWin source rather than a live Plasma session.
Before submitting#
- Related issues: no matching report found in searches of open and closed issues and pull requests for KDE permissions and percent-containing capture paths.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/desktop
Observed behavior#
The focused fixture creates synthetic helper files under a normal data directory and a directory containing %, invokes the repository's actual desktop-entry encoder, and passes the generated command to Qt's QProcess.splitCommand and QFileInfo.canonicalFilePath, matching KWin 6.7.6's executable comparison. The normal-directory control matches, but the percent-directory entry contains data%%test while the installed helper is under data%test, so the canonical executable comparison fails.
Source-only desktop consequence: KWin 6.7.6 cannot find this helper's desktop entry through that comparison and denies its screenshot interface. T3's readiness probe then returns the instruction to reinstall the helper, but reinstalling generates the same doubled-percent path. No Linux desktop, service cache, or screenshot was run for this investigation.
Reproduction#
- Use T3 Code commit
59e64eef881632a23f89c4138bcf37f23d92bf93and Python withPySide6-Essentials6.12.0 plus Node with native TypeScript support. - Run the script below against the checkout. It extracts and runs the checked-out encoding functions, creates only synthetic files, and invokes Qt's actual executable-token parser and canonical path lookup.
- Compare the passing ordinary-directory control with the percent-containing directory. The latter fails the same executable lookup KWin 6.7.6 uses for authorization.
One completed fixture run reproduced the mismatch. An earlier fixture attempt was inconclusive because its function extraction omitted escapeDesktopEntryString; the script below includes both functions. Full SnapShot setup and capture on Linux were not run. NOT_ENOUGH_INFO: a disposable Plasma 6.7.6 session would establish the desktop error end to end, including its actual KService cache.
Save the script as repro.sh and run sh repro.sh /path/to/t3code. Set PYTHON to an interpreter with PySide6-Essentials installed, and optionally NODE to the Node executable. All generated inputs, HOME, TMPDIR, and XDG directories are disposable and removed on exit; the checkout is read only. Exit 1 means the mismatch reproduced, exit 0 means both paths match, and exit 2 means setup or the control failed. This tests the authorization lookup boundary, not a running compositor.
Reproduction script: repro.sh
#!/bin/sh
set -u
if [ "$#" -ne 1 ]; then
echo "RESULT: inconclusive"
exit 2
fi
PYTHON=${PYTHON:-python3}
NODE=${NODE:-node}
probe=$(mktemp -d /tmp/kde-permission-probe.XXXXXX) || exit 2
trap 'rm -rf "$probe"' EXIT HUP INT TERM
export HOME="$probe/home" TMPDIR="$probe/tmp"
export XDG_DATA_HOME="$probe/data" XDG_CONFIG_HOME="$probe/config"
export XDG_STATE_HOME="$probe/state" XDG_CACHE_HOME="$probe/cache"
mkdir -p "$HOME" "$TMPDIR" "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"
"$PYTHON" - "$1" "$probe" "$NODE" <<'PY'
import json
import pathlib
import subprocess
import sys
try:
from PySide6.QtCore import QFileInfo, QProcess, qVersion
repo, scratch = map(pathlib.Path, sys.argv[1:3])
node = sys.argv[3]
print("Python " + sys.version.split()[0])
print("Qt " + qVersion())
print("Node " + subprocess.check_output([node, "--version"], text=True).strip())
source = (repo / "apps/desktop/src/app/DesktopLinuxUrlHandler.ts").read_text()
start = source.index("const escapeDesktopEntryString =")
function_start = source.index("export function escapeDesktopEntryExecArgument(", start)
end = source.index("\n}\n", function_start) + 3
# Execute the checked-out TypeScript function without importing the Electron app.
targets = [scratch / "normal data/t3code/kde-capture/t3-kde-snap-shot",
scratch / "data%test/t3code/kde-capture/t3-kde-snap-shot"]
for target in targets:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"synthetic helper identity; never executed")
module = scratch / "encoder.ts"
module.write_text(source[start:end] + "\nconsole.log(JSON.stringify(" + json.dumps([str(p) for p in targets]) + ".map(escapeDesktopEntryExecArgument)));\n")
encoded = json.loads(subprocess.check_output([node, str(module)], text=True))
outcomes = []
for label, target, command in zip(["control", "percent"], targets, encoded):
# Neither fixture needs KConfig backslash decoding. KService preserves the
# percent field codes in Exec; KWin 6.7.6 then uses splitCommand directly.
if "\\" in command:
raise RuntimeError("fixture unexpectedly needs KConfig decoding")
first = QProcess.splitCommand(command + " check")[0]
matched = QFileInfo(first).canonicalFilePath() == QFileInfo(str(target)).canonicalFilePath()
outcomes.append(matched)
print(label + " desktop Exec=" + command.replace(str(scratch), "$SCRATCH") + " check")
print(label + " KWin executable match=" + str(matched).lower())
if not outcomes[0]:
raise RuntimeError("normal-directory control failed")
status = 0 if outcomes[1] else 1
except Exception as error:
print(type(error).__name__ + ": " + str(error))
status = 2
print("RESULT: " + {0: "expected behavior", 1: "defect reproduced", 2: "inconclusive"}[status])
sys.exit(status)
PY
Expected behavior#
A helper installed under the configured XDG data directory should resolve to the same executable identity when KWin reads its desktop entry. T3 explicitly derives the applications directory from XDG_DATA_HOME without excluding percent signs in apps/desktop/src/app/DesktopEnvironment.ts#L181-L184, and installs the helper beneath that directory in apps/desktop/src/snapShot/KdeSnapShot.ts#L25-L43. KWin's actual supported permission contract uses the first token of QProcess::splitCommand(service->exec()) as the canonical executable path in src/utils/serviceutils.h#L28-L45.
Impact and scope#
Impact: P2 - A valid but unusual configured data path prevents KDE SnapShot authorization on KWin 6.7.6. The executable lookup mismatch is reproduced; the application-level denial follows the tagged source and was not run on Linux.
The ordinary-directory control matches successfully. Choosing a percent-free data path avoids this specific parser mismatch, but changing an existing installation's data directory and completing setup were not tested. NOT_ENOUGH_INFO: the number of affected installations and the earliest affected compositor release are unknown. KWin 6.8.3 has removed this permission lookup, so this report does not claim the same failure there.
Version#
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93. KWin permission implementation examined at tag v6.7.6, with v6.8.3 checked separately. Fixture versions: Python 3.15.2, Node v26.12.1, PySide6-Essentials 6.12.0, Qt 6.12.0.
Runtime and platform#
Fixture executed on macOS arm64, using QtCore only and synthetic files. The Linux behavior is established from KWin 6.7.6 source and KService 6.20.0 source, not a running Linux compositor. NOT_ENOUGH_INFO: exact Linux distribution and installed KService version; a disposable matching Plasma session would supply them.
Interface#
UI: Settings → SnapShots → install KDE helper. Integration: KWin ScreenShot2 readiness check and desktop-entry executable authorization.
Earliest failure stage#
Setup or configuration.
Relevant output or evidence#
The completed fixture printed the following output and exited 1. $SCRATCH replaces its random temporary root; no real helper, bus, credentials, or windows were used.
Python 3.15.2
Qt 6.12.0
Node v26.12.1
control desktop Exec="$SCRATCH/normal data/t3code/kde-capture/t3-kde-snap-shot" check
control KWin executable match=true
percent desktop Exec="$SCRATCH/data%%test/t3code/kde-capture/t3-kde-snap-shot" check
percent KWin executable match=false
RESULT: defect reproduced
- T3 generates the KDE desktop
Execfield through the shared encoder in apps/desktop/src/snapShot/KdeSnapShot.ts#L32-L43. The encoder doubles percent signs in apps/desktop/src/app/DesktopLinuxUrlHandler.ts#L60-L69. - KService retains the desktop
Execvalue, including field codes, in src/services/kservice.cpp#L73-L77 and returns it throughexec()in src/services/kservice.cpp#L614-L618. The fixture does not instantiate KService; for these two paths there are no backslash escapes to decode. - KWin compares that token with the caller's real executable path and returns no interfaces when it finds no service in src/utils/serviceutils.h#L28-L65. Its permission check rejects missing interfaces before resolving the window in src/plugins/screenshot/screenshotdbusinterface2.cpp#L244-L299.
- T3 maps that denial to a reinstall message in apps/desktop/src/snapShot/KdeSnapShot.ts#L65-L72, and reinstall rewrites the same generated entry in apps/desktop/src/snapShot/KdeSnapShot.ts#L165-L177.
- Release distinction: src/plugins/screenshot/screenshotdbusinterface2.cpp at v6.8.3 omits the desktop-entry permission check; receiving
InvalidWindowthere does not prove the historical executable lookup succeeded.
tanstack-source added labelsbug
juliusmarminge closed this
Sign in to join this conversation.
I tried to reproduce this on the current build and could not.
Commit b7a224c7dd (origin/main). No Plasma 6.7 session is available (Ubuntu 24.04 offers only KWin 5.27; T3's KDE helper requires Plasma 6 / kbuildsycoca6), so the end-to-end KWin denial was not observed in a running app. Verified the authorization boundary instead: (1) read KWin v6.7.6 src/utils/serviceutils.h, which matches services by QFileInfo(QProcess::splitCommand(service->exec()).first()).canonicalFilePath() == executablePath with no %% decoding; (2) ran main's escapeDesktopEntryExecArgument through real Qt 6.12 (PySide6) QProcess.splitCommand + canonicalFilePath on synthetic files: the normal-folder control matched, but data%test was encoded as "…/data%%test/…" and did not match; (3) added a test to apps/desktop/src/snapShot/KdeSnapShot.test.ts that runs KdeCaptureSetup.perform('install-kde-helper') with dataHome '/data%test', reads the written Exec line, and splits it the way KWin does. It fails on main: expected '.../data%test/t3code/kde-capture/t3-kde-snap-shot', received '.../data%%test/...'. The other 9 tests in the file pass.
Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.