Repository navigation
Description
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:
- Unquoted path: a peer name carrying only an override (no tab/newline) is returned byte-identical by
plain_field. - 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 charactersbreaks_rowmatches — 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=falsefor 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:
format_peer_row(src/commands/peers.rs:206-219):<node_id>\t<name>\t<blob_count>\t<timestamp>— the name arrives over unsigned gossip, is stored and served verbatim (peers.rs:116-121), so any peer can announce such a name. In a terminal, U+202E reverses the visual order of the text after it: the blob-count/timestamp columns can be made to read swapped or falsified, defeating the column structure id(peers): a peer's announced name is printed raw — a newline in a gossip announcement forges extra TSV rows in id peers and the REPL #1613 exists to protect.- Same renderer in the REPL peer list (
src/commands/repl.rs:116-123).
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_fieldquotes 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.rscovering: override-only name is quoted+escaped; override+tab name has no raw override inside the quotes. - The sibling surfaces (
group.rstext(),world/util.rssanitizers) 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
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
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.mdin 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:
petname.rs'sis_format_char— including U+200B–U+200D and U+FEFF — promoted to one sharedpub(crate)definition by this phase. No renderer-local subset.group.rstext()is fixed as Phase 2 (id(group/doc/claim): the shared key=value renderer passes bidi/format controls (Cf) raw — a claim value with U+202E reaches the terminal unescaped in both the unquoted and quoted paths and visually reorders the row's author=/event= fields #3111); theworld/util.rssanitizers are dispositioned by the closing audit, Phase 3b (Phase 3b (audit) of #3455 — closing is_control inventory + plain-output contract statement #3456). Both are filed and linked as sub-issues of Design: a value cannot act on the terminal — one format-character (Cf) contract for pkgs/id plain output #3455 — no separate follow-ups needed from this issue's triage.plain_field.58 remaining items
dezren39 commentedon Nov 2, 2026
🤖 Agent note · parked (cargo load gate)
cargo test -p id commands::text::tests+ petname tests + id suite on the laptop worktree~/git/fleet/5439@90782ce75 → merge PR fix(id): plain_field escapes the shared Unicode 16 Cf set (#2839) #4264 → 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 closes → complete task 5439 (also clears the Phase-1 gate for task 5635 / id(group/doc/claim): the shared key=value renderer passes bidi/format controls (Cf) raw — a claim value with U+202E reaches the terminal unescaped in both the unquoted and quoted paths and visually reorders the row's author=/event= fields #3111).dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
fix(id): plain_field escapes the shared Unicode 16 Cf set (#2839) (#4264
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done