Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

`ESC k <text> ESC \` (screen/tmux title sequence) leaks payload as visible text

Đang mở
#153 2 bình luận 0 reaction 1 người được giao Xem trên GitHub

@ThomasK33 đang làm issue này rồi.

Từ ngày 26/6/2026.

Đánh giá

Issue này chưa được đánh giá.

Mô tả

accepted bug triage:done

Problem

When the WASM parser encounters ESC k <text> ESC \, it logs unimplemented ESC action: ESC k and then renders <text> onto the terminal grid, silently consuming the trailing ESC \. This is the GNU screen / tmux title-setting extension — the payload should never appear on the display.

OSC 0; / 2; title-setting in the same build works correctly (payload is consumed, not painted), so this is specific to the ESC k code path.

Expected result

0: ""
1: "demo.txt"

Neither title string (/tmp, ls) should appear on the grid.

Actual result

0: "/tmp"
1: "lsdemo.txt"

Both title strings leak. The second one is jammed into the first line of visible output, which on a real shell manifests as the running command's name concatenated with whatever it prints.

Console (per term.write() call):

[ghostty-vt] warning(stream): unimplemented ESC action: ESC  k
[ghostty-vt] warning(stream): unimplemented ESC action: ESC  k

Also verified:

  • Same result whether term.write() is called with Uint8Array or with a plain string.
  • Same build correctly consumes ESC ] 0 ; /tmp BEL (OSC 0) without leaking.

Reproduction

Standalone. ghostty-web@0.4.0, headless Chromium 146, macOS 15 arm64, Node 25.

mkdir ghostty-repro && cd ghostty-repro
npm init -y >/dev/null
npm install ghostty-web puppeteer
# save the file below as repro.mjs, then:
node repro.mjs

repro.mjs:

import http from "node:http";
import fs from "node:fs/promises";
import path from "node:path";
import puppeteer from "puppeteer";

const ROOT = path.dirname(new URL(import.meta.url).pathname);
const PORT = 5765;

// WASM needs to load over http(s), not file://, so serve the installed
// ghostty-web package from a tiny local static server.
const server = http.createServer(async (req, res) => {
  const url = req.url === "/" ? "/index.html" : req.url;
  const local = url.startsWith("/ghostty-web/")
    ? path.join(ROOT, "node_modules/ghostty-web", url.slice("/ghostty-web/".length))
    : path.join(ROOT, url);
  try {
    const data = await fs.readFile(local);
    const ext = path.extname(local);
    res.writeHead(200, {
      "Content-Type":
        ext === ".js" || ext === ".mjs" ? "text/javascript" :
        ext === ".wasm" ? "application/wasm" :
        ext === ".html" ? "text/html" : "application/octet-stream",
    });
    res.end(data);
  } catch {
    res.writeHead(404).end();
  }
});
await new Promise(r => server.listen(PORT, r));

await fs.writeFile(
  path.join(ROOT, "index.html"),
  `<!doctype html><meta charset="utf-8"><body><div id="t"></div></body>`,
);

const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
page.on("console", m => {
  const t = m.text();
  if (t.includes("unimplemented") || t.includes("warning")) console.log("  [page]", t);
});

await page.goto(`http://localhost:${PORT}/`);

const lines = await page.evaluate(async () => {
  const mod = await import("/ghostty-web/dist/ghostty-web.js");
  const ghostty = await mod.Ghostty.load();
  const term = new mod.Terminal({ cols: 80, rows: 5, ghostty });
  term.open(document.getElementById("t"));

  // ESC k /tmp ESC \       (screen/tmux title-set #1)
  // \r\n
  // ESC k ls  ESC \        (screen/tmux title-set #2)
  // demo.txt\r\n           (visible body)
  term.write(new Uint8Array([
    0x1b, 0x6b, 0x2f, 0x74, 0x6d, 0x70, 0x1b, 0x5c,
    0x0d, 0x0a,
    0x1b, 0x6b, 0x6c, 0x73, 0x1b, 0x5c,
    0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x0d, 0x0a,
  ]));

  await new Promise(r => setTimeout(r, 150));

  const out = [];
  const active = term.buffer.active;
  for (let y = 0; y < term.rows; y++) {
    const line = active.getLine(y);
    if (line) out.push(line.translateToString(true));
  }
  return out;
});

console.log("\nRendered buffer:");
lines.forEach((l, i) => console.log(`  ${i}: ${JSON.stringify(l)}`));

await browser.close();
server.close();

Running it prints:

  [page] [ghostty-vt] warning(stream): unimplemented ESC action: ESC  k
  [page] [ghostty-vt] warning(stream): unimplemented ESC action: ESC  k

Rendered buffer:
  0: "/tmp"
  1: "lsdemo.txt"
  2: ""
  3: ""
  4: ""

Suggested fix

Recognize ESC k <text> ESC \ as the screen/tmux title-setting extension and consume it without rendering the payload. Routing <text> to a title-change listener (as OSC 0/2 already do) would be a bonus but isn't required — silently discarding is enough to fix the visible bug.

Ngôn ngữ chính
TypeScript
Star
2.9k
Fork
174
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của coder/ghostty-web

Tất cả issue của coder/ghostty-web

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.