Files
ltk/SECURITY.md
Pedro M. de Echanove Pasquin 3d8523533c
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
text-input: re-sync on enter and flag secure fields as Password
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.
2026-05-27 22:31:36 +02:00

6.6 KiB
Raw Blame History

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 ltk version 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 ltk crate root or its core::UiSurface runtime-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 secure mode (currently TextEdit) — 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 via secure_mem::secure_zero (volatile writes + a compiler_fence so the optimiser cannot elide the wipe). The guarantee covers two copies the runtime owns: the TextEdit itself and the per-frame WidgetHandlers::TextEdit snapshot the layout pass produces for input dispatch — both implement Drop and run the wipe before the allocation returns to the allocator. The next view() 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 flagged Password with SensitiveData | 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::secure does not cover

    The wipe only reaches buffers ltk allocated. Callers retain responsibility for everything outside the widget tree:

    • Application-owned Strings. text_edit( "Pwd", &self.password ) only borrows self.password; the credential lives on the consumer's struct. Wipe it explicitly (via secure_mem::secure_zero on password.as_bytes_mut()) once the auth handshake completes, or put the credential in a wrapper type with a Drop impl that does the wipe.
    • on_change callback copies. Each keystroke fires a closure with a fresh String clone 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's PR_SET_DUMPABLE to 0, and consider running on a no-swap mount. ltk itself does not call mlock because the buffers it owns are short-lived (single frame) and mlock-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 secure paints 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::secure with an explicit wipe of their own state on the success path. The docs/cookbook.md "Password field with PAM submit" recipe shows the canonical shape: clear self.password after 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.

  • draw_image_data (both backends) refuses to upload a buffer whose length does not match width × 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.