LTK drives the on-screen keyboard through zwp_text_input_v3: focusing a text widget enables text-input so the compositor's input-method (squeekboard) brings the keyboard up. Two gaps are fixed here. Handle the `enter` event by re-emitting enable + content type + commit. The compositor sends `enter` when it (re)focuses the text input — notably when an input-method connects after the field has already enabled text-input (a startup race). LTK previously ignored `enter`, so that activation was lost and the keyboard never appeared; it now re-declares its state and the OSK comes up. Declare the content type from the field's `secure` flag: secure fields are flagged `ContentPurpose::Password` with `ContentHint::SensitiveData | HiddenText` (so the IME/OSK skips prediction, autocorrect and storing the value), everything else stays `Normal`/`None`. The content type is also refreshed when focus moves between two text fields (e.g. username → password) without re-creating the text-input object, and a new `AppData::text_input_secure` lets the `enter` re-emit path preserve the current field's type. Correct the docs: `TextEdit::secure()` and SECURITY.md claimed secure "skips text-input-v3 registration". It does not — the field still activates text-input so the OSK works on it; the protection is the Password / sensitive flagging, and the value still reaches a trusted compositor/IME.
6.6 KiB
Security policy
ltk is a UI toolkit for Wayland clients. It runs inside the trust boundary
of the application that links it: the toolkit decodes images, parses theme
JSON, lays out widget trees and drives a GLES / software renderer. A bug in
any of those paths can crash the host application or, in worst-case, expose
information that a co-tenant Wayland client should not have.
This document describes how to report a vulnerability and what we consider in or out of scope.
Supported versions
ltk is in active development. Security fixes land on the latest released
minor version on crates.io and on the master branch in this repository.
Older minor versions are not patched.
Reporting a vulnerability
Email info@liberux.net with:
- a description of the issue,
- the smallest reproducer you can share (Rust source preferred),
- the
ltkversion and Rust toolchain you used, - whether the issue is exploitable today or requires a follow-up condition.
Please do not open a public GitHub issue, pull request, or discussion for unpatched vulnerabilities. We aim to acknowledge reports within 5 working days and ship a fix within 30 days for high-severity issues. If you do not hear back, escalate by sending a follow-up email.
You may use PGP if you prefer — request the current public key in your first message and we will reply with it before sharing any sensitive details.
In scope
- memory-safety bugs reachable from any public API on the
ltkcrate root or itscore::UiSurfaceruntime-free surface, - panics or undefined behaviour triggered by a theme JSON document, a font file, or an image buffer that an application would reasonably accept from user content (e.g. a wallpaper picker, a notification icon),
- denial-of-service through unbounded memory growth in caches or layout recursion that the toolkit performs on behalf of the application,
- credential leaks in widgets that expose a
securemode (currentlyTextEdit) — for example, plaintext password residue in heap dumps after the widget is dropped.
Out of scope
- bugs that require a malicious Wayland compositor: ltk treats the compositor as part of the trusted computing base and does not defend against compositor-level spoofing, layer-shell privilege misuse, or protocol-level attacks,
- bugs that require an attacker to set environment variables on the user's
process (
LTK_THEMES_DIR,LTK_FORCE_SOFTWARE,XDG_DATA_HOME): ltk is not designed to run inside setuid binaries or anywhere the env is not under the user's control, - bugs in third-party dependencies (
tiny-skia,fontdue,image,smithay-client-toolkit, …): report those upstream. We will track the fix and bump the dependency in a release note.
Hardening features
-
TextEdit::secure( true )zeroizes the underlying string buffer on drop viasecure_mem::secure_zero(volatile writes + acompiler_fenceso the optimiser cannot elide the wipe). The guarantee covers two copies the runtime owns: theTextEdititself and the per-frameWidgetHandlers::TextEditsnapshot the layout pass produces for input dispatch — both implementDropand run the wipe before the allocation returns to the allocator. The nextview()rebuild replaces the previous frame's snapshot, so the typical lifecycle is "one frame on the heap, then overwritten on drop". Single-line and multiline are mutually exclusive with secure (passwords have no line breaks). Note: secure does not skip the text-input-v3 path — the field still activates it (so the on-screen keyboard works on password fields) but is flaggedPasswordwithSensitiveData | HiddenText, which asks the IME / OSK not to predict, autocorrect or store the value. The value can still reach a trusted compositor/IME; closing that path would mean not activating text-input there (and losing the OSK on the field).What
TextEdit::securedoes not coverThe wipe only reaches buffers ltk allocated. Callers retain responsibility for everything outside the widget tree:
- Application-owned
Strings.text_edit( "Pwd", &self.password )only borrowsself.password; the credential lives on the consumer's struct. Wipe it explicitly (viasecure_mem::secure_zeroonpassword.as_bytes_mut()) once the auth handshake completes, or put the credential in a wrapper type with aDropimpl that does the wipe. on_changecallback copies. Each keystroke fires a closure with a freshStringclone of the current value. If the closure stores or forwards that value (e.g. to a worker thread for PAM), each stored copy is the consumer's responsibility — ltk no longer owns it past the closure call.- OS-level disclosure surfaces. Swap, hibernation images, and
core dumps are outside any user-space wipe's reach. For threat
models that require resistance to these, link an
mlock-aware allocator, restrict the process'sPR_SET_DUMPABLEto0, and consider running on a no-swap mount. ltk itself does not callmlockbecause the buffers it owns are short-lived (single frame) andmlock-ing them per-frame would dominate the cost without materially improving the threat model. - Compositor-side state. The compositor sees pointer / keyboard
events for the surface but never the rendered glyphs (since
securepaints bullets). Custom IMEs that the user has installed can still observe keystrokes — that is an OS-level concern outside ltk's trust boundary.
Login / lock-screen consumers should pair
TextEdit::securewith an explicit wipe of their own state on the success path. Thedocs/cookbook.md"Password field with PAM submit" recipe shows the canonical shape: clearself.passwordafter spawning the worker thread so the in-flight clone in the worker is the only remaining copy, and let the worker's drop-on-completion run its own wipe. - Application-owned
-
draw_image_data(both backends) refuses to upload a buffer whose length does not matchwidth × height × 4. The mismatch path logs once and draws nothing. -
Glyph caches and gradient stops have soft caps to bound memory growth under unusual content. Exceeding the cap drains older entries (glyphs) or truncates the input (gradient stops) with a stderr warning.
Supply chain
ltk targets the Rust toolchain shipped with Debian stable (currently 1.85).
Direct dependencies are pinned at the minor-version level except image,
which is pinned to an exact version because the crate's MSRV and feature
surface change between patch releases. We monitor RustSec advisories
manually; running cargo audit in your own CI is recommended.