Skip to content

id(peers): plain_field passes bidi/format controls raw — a gossip peer name with U+202E reaches the terminal unescaped and visually reorders the row's columns #2839

Closed

Description

@dezren39

Problem

plain_field (pkgs/id/src/commands/text.rs:42-44) — the renderer whose whole job is making untrusted values row-safe in plain-text output (#1613) — treats a value as structure-breaking only when it contains a char::is_control character or U+2028/U+2029:

fn breaks_row(c: char) -> bool {
    c.is_control() || matches!(c, '\u{2028}' | '\u{2029}')
}

Unicode bidirectional and format controls are category Cf, not Cc, so is_control() is false for all of them: U+202A–U+202E (embedding/override, incl. RIGHT-TO-LEFT OVERRIDE), U+2066–U+2069 (isolates), U+200E/U+200F (LRM/RLM), U+200B, U+FEFF. They reach the terminal raw in both paths:

  1. Unquoted path: a peer name carrying only an override (no tab/newline) is returned byte-identical by plain_field.
  2. Quoted path: even when quoting does trigger (e.g. the name also contains a tab), the escape loop in plain_field (text.rs:62-86) only escapes characters breaks_row matches — the override is emitted raw inside the quotes, where it still acts on the terminal rendering.

Evidence (verified this run, main a8ea41ae)

Probe crate on the laptop (~/git/fleet/probe-2026-10-30-laneA-bidi/) running breaks_row/plain_field copied verbatim from src/commands/text.rs:

  • U+202A..U+202E, U+2066..U+2069, U+200E, U+200F, U+200B, U+FEFF: is_control=false, breaks_row=false for every one.
  • plain_field("mallory\u{202e}9 0") returns the input byte-identical, unquoted, with a raw U+202E in the output.
  • A name with a tab + U+202E is quoted, but the output still contains the raw U+202E.

Callers put the result in fixed-column rows whose trailing fields are exactly what an override reorders visually:

Sibling surfaces with the same is_control-only assumption, to audit in the same pass (not necessarily the same fix): the key=value renderer text() in src/commands/group.rs:66 (quotes only on whitespace/control; serde_json string rendering does not escape Cf either) and sanitize_display_name/sanitize_chat in src/world/util.rs:69-84 (filter only is_control).

Why it matters

The #1611/#1613 ruling is that a printer never lets an untrusted value be mistaken for structure. A bidi override does not add a row or shift a byte column — it corrupts what the row appears to say, which is the terminal-level equivalent: a malicious peer name can visually falsify the numeric columns next to it. This is the Trojan-Source class of characters (CVE-2021-42574) applied to live CLI output.

Acceptance criteria

  • plain_field quotes and escapes (\uXXXX) the bidi/format controls — at minimum U+202A–U+202E, U+2066–U+2069, U+200E, U+200F — whether they appear alone or alongside an already-escaping character; a rendered field never contains a raw Cf override/isolate/mark.
  • Ordinary names are unchanged byte-for-byte: spaces, CJK, emoji, accented letters (the existing test corpus in text.rs) still render unwrapped.
  • Regression tests in src/commands/text.rs covering: override-only name is quoted+escaped; override+tab name has no raw override inside the quotes.
  • The sibling surfaces (group.rs text(), world/util.rs sanitizers) are audited and either fixed in the same PR or explicitly filed as follow-ups from triage.
  • Triage pins the exact character set (embeddings/overrides/isolates/marks, and whether zero-width chars U+200B–U+200D/U+FEFF are in scope) before the fix packet is routed.

Dedup checked this run: open+closed search for bidi/plain_field/Trojan/unicode-spoof — no hits; #1611/#1613/#1856 cover tab/newline/space escaping only.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A)
    • Session / ID: new-issue-creator-code-a, cron run 2026-10-30 16:35 CDT
    • Found via: filed this run
    • Task: Filed after verifying with a laptop probe (verbatim breaks_row/plain_field from main a8ea41a) that bidi/format controls (U+202A–U+202E, U+2066–U+2069, U+200E/F) are not is_control, pass plain_field raw — byte-identical when alone, raw inside quotes when a tab also triggers quoting — and so reach the id peers / REPL rows from unsigned gossip names.
    • Expectation: effort/xs triage — pin the exact Cf character set, then route a small_fix packet.
    • Links: probe laptop ~/git/fleet/probe-2026-10-30-laneA-bidi/; evidence in issue body.
  3. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Adopted as Phase 1 of design #3455 (a value cannot act on the terminal — one format-character contract for pkgs/id plain output; design doc files/designs/code-id-plain-output-rendering.md in the keep-the-four-repos-healthy goal workspace). Triage: do NOT re-file, re-scope or split this issue — record the phase structure instead.

    Decisions recorded on #3455 that bind this phase:

  4. added
    work/assignedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 58 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked (cargo load gate)

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions