Skip to content
gwmgwmgwmv1.10.0

v1.6.1

Follow-up to the 1.6.0 security release. The neutralisation that shipped there rests on char::is_control, which covers C0, DEL and C1. It does not cover the twelve characters carrying the Bidi_Control property: those are Cf, not Cc, and they reorder how a terminal renders the text around them without ever being a control byte. The pre-trust bootstrap summary inherited the gap, and that is the one output whose whole job is to let someone authorise a shell command out of a repo they have not vetted. The TUI worktrees table was never on the path the CLI sinks protect at all, and what had shielded it so far was incidental to ratatui’s rendering. Both are closed here, along with an alias expansion that becomes argv before clap can be reached.

The rest of the fixes are test hardening around the same class of mistake: a rule stated in a doc comment rather than checked, and two races in the harness that were never reachable from the product.

Two additions travel with it. The published documentation now redeploys itself when main moves, and herdr-plugin-gwm gets an integration page in English and French.

  • The published documentation now follows main. A merge into main that touches docs/, changelogs/ or Cargo.toml posts a repository_dispatch to kbrdn1/kbrdn-docs, which re-runs the conversion, commits whatever drifted and redeploys the site. This repository owns the Markdown sources and the trigger, nothing else: doing the conversion here would pull a Bun monorepo into a CI that cannot verify its build.

    main rather than the tag, because main is only reached through a dev to main pull request, so what lands there is what was delivered. A tag would be worse than a shortcut: the v*.*.* glob also matches v0.8.0-rc.4, the trap release.yml already guards by hand, so every release candidate’s documentation would go live as if it were stable. Cargo.toml is watched because the site reads the [package] version for the release badge in its header, and a bump can travel without touching changelogs/. (#423)

  • An integration page for herdr-plugin-gwm, in English and French. herdr is a terminal multiplexer with workspaces, panes and a plugin API; the plugin wires gwm into it, so create, switch, remove, review a pull request, exec and clean all reach across worktrees from inside the multiplexer, with gwm’s own TUI available in a pane.

    The page leads with the rule the plugin is built around: it never creates a worktree on the herdr side. Creation and removal always go through gwm, and herdr only adopts what gwm produced. That is what keeps a single source of truth, and the plugin enforces it rather than documenting it, with one helper as the only path to herdr and a grep-assert failing the build if any script reaches around it. (#511)

  • The rule that a test which can observe a rewritten environment variable must hold its binary’s lock is now checked by construction rather than stated in a doc comment. That comment is what #503 was about: it named a boundary narrower than the real one, and five tests were written against the sentence. Its replacement then stated a second wrong boundary, that a layered load reads $HOME. It does not: Config::load_layered and resolved_rows take the global config path as a parameter, and Config::load_for_repo, which does resolve it, is called by no test in that binary. A new test walks the four test binaries that rewrite a variable and fails on any test calling an ambient reader of it without the lock, ignoring mentions in comments and identifiers that merely end with the name. Both mistakes came from a search that could not tell those apart. (#507)

  • A branch name no longer reaches the TUI table with its bidi controls intact. The sinks the CLI goes through are not on the TUI’s path, and what protected the TUI so far was incidental: measured on ratatui 0.30, every render path drops the zero-width control bytes, but List and Table keep the Bidi_Control characters. The worktrees table renders through Table, and git’s ref rules refuse the ASCII controls and ~^:?*[ but not the Unicode format characters, so a fetched ref could carry one and its row could read in an order the ref is not stored in.

    The neutralisation goes in the width-clipping funnel every constrained cell already passes through, rather than at each cell, so a column added later inherits it. It runs before the width count, so what is measured is what is drawn: a Bidi_Control character can measure zero columns where its ? measures one. The path column, which is not width-constrained, says so itself. tui::wt_tree::sanitize_name now delegates to the shared predicate instead of keeping a copy that had drifted: a filename out of git status -z reorders a sidebar row exactly as a ref name reorders a table row. (#506)

  • A config value no longer reaches the terminal with its Unicode bidi controls intact. The neutralisation added in 1.6.0 replaces control characters, and char::is_control covers C0, DEL and C1: it does not cover the twelve characters carrying the Bidi_Control property, which are Cf, not Cc. They reorder how a terminal renders the text around them without ever being a control byte, so a value carrying one can display something other than what it is.

    The site that matters is the pre-trust bootstrap summary, whose whole job is to let someone decide whether to authorise a shell command out of a repo they have not vetted. A summary that can be made to misrepresent the command it asks about is worse than no summary. gwm config get, types, trust list and the rendered diagnostics carry the same exposure at lower stakes, and all of them inherit the fix: it lands in the two sinks every echo site already goes through, the way they inherited the control-character rule. An [aliases] expansion is refused rather than neutralised, for the reason it already was in 1.6.0: it becomes argv before clap parses it, and clap prints its own error without passing through gwm’s output path, so nope<ALM>abc came back out of it with the character intact.

    The set is Bidi_Control exactly rather than the overrides and isolates alone, because the three implicit marks reorder too: U+061C is bidi class AL and U+200F is R, so either one is a strong right-to-left character inside left-to-right text and the weak and neutral rules of UAX #9 then reorder the digits and punctuation around it. An argument or a URL can be made to render in an order the bytes do not have. This is a gap in the 1.6.0 mitigation, not a regression: 1.5.0 and earlier neutralised nothing at all. (#502)

  • A race in the test harness, not reachable from the product. exec_in_dir_runs_a_relative_script_from_the_worktree writes an executable and immediately runs it, and execve refuses a file that is open for writing by any process: a child forked by another test thread carries a copy of that write handle until it execs, so the spawn intermittently returned ETXTBSY on Linux. It is retried on that errno alone, with the reason written at the retry, and every other spawn error still fails on the first attempt. (#500)

  • The same shape in config_tests, where the guard around $HOME documented a boundary narrower than the real one, and five tests skipped it on the strength of that doc. expand_placeholders resolves the home directory before it looks at a single token, so every call is a concurrent reader whatever the template says, while one test rewrites the variable with set_var. The guard is now taken by every test in that binary that can observe $HOME, and its doc states the hazard rather than a proxy for it. (#503)