# 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 `String`s.** `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`](./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.
