在 `/usr/bin/top` 内部进行的一项寄生式斐波那契计算
A Minsky machine in ncurses terminfo

原始链接: https://seriot.ch/computation/terminfo/

Nicolás Seriot 的文章表明,ncurses terminfo 的参数扩展可以充当一种小型、有状态的编程语言。它支持持久化寄存器、整数运算、比较、条件分支、输入参数和输出,但没有内部循环。反复进行能力扩展可以作为外部时钟。 作者将程序计数器编码为一个寄存器,并使用两个计数器,从而构造出一台双计数器米斯基机。示例中的 terminfo 规则实现了加法和斐波那契数列,每扩展一次,机器就推进一个步骤。一个更不同寻常的示例使用 `/usr/bin/top` 作为时钟:当 `top` 重新绘制秒数字段并请求特定光标位置时,对应的 terminfo 规则会推进一个“寄生的”斐波那契程序,并将结果显示在终端标题中。 在计数器、规则大小和执行步骤数均不受限制的理想化假设下,这种构造具有计算通用性。实际实现存在有限限制,因此属于有限状态系统。这本身并不构成安全漏洞:terminfo 扩展无法直接访问文件、执行命令或发起系统调用。不过,以特权运行的程序中的解析器或求值器缺陷仍可能造成安全问题。

一则 Hacker News 帖子介绍了“在 ncurses terminfo 中实现 Minsky 机器”,展示如何通过重复进行 terminfo 参数展开,模拟一台双计数器 Minsky 机器,从而实现计算通用性。 讨论中有人批评 terminfo 已经过时,只是一个静态的能力数据库。现代软件终端本可以通过 `XTGETTCAP` 等查询来报告自身功能,但这类查询的支持仍然有限。另一些人则为 terminfo 辩护,认为它是一个有用的翻译层:用户可以添加系统级或用户级条目,不受支持的功能也可以优雅降级。他们还指出,在硬件价格高昂的年代,查询终端的成本很高;而如今许多兼容性问题反而源于应用程序忽略 `TERM`,或者硬编码转义序列。 几位评论者质疑这个项目的创新性,提到了 2019 年的一篇类似文章,并认为借助 AI 辅助写作会让这类发现显得没那么令人印象深刻。还有人开玩笑地问,它能不能运行《DOOM》。
相关文章

原文
A Minsky machine in ncurses terminfo

Nicolas Seriot

Computation > A Minsky machine in ncurses terminfo

ncurses terminfo parameter expansion can simulate 2-counter Minsky machines.

Host a parasite Fibonacci program in your terminal, clocked by /usr/bin/top.

2nd October 2026

1. Introduction

Back in 2019, Gwen Weinholt (weinholt.se) noticed that Terminfo featured a stack machine with parameters, arithmetic and logic, if-then-else, output, and persistent variables. Gwen noted that terminfo was close to a Turing machine but lacked loops, which could be worked around by pushing the iteration outside the language.

More recently, Martin Tournoij implemented the Go termfo package and noted that "terminfo files are Turing-complete".

This article builds on these observations and makes the universality argument explicit with a reduction from two-counter Minsky machines.

2. Terminfo is a Small Programming Language

Early physical terminals used various escape sequences to move cursor, delete characters, write in bold or colors, etc. Terminal applications need to know which escape sequences a terminal understands. The $TERM environment variable names a terminal type, and the terminfo database describes its capabilities. The database is actually a set of compiled keys and values, usually stored in /usr/share/terminfo.

A typical macOS Terminal profile declares TERM=xterm-256color. Running infocmp xterm-256color shows, among other capabilities: cup=\E[%i%p1%d;%p2%dH. cup is the key used for cursor addressing. Curses supplies zero-based row and column arguments, and %i increments the first two parameters, because the terminal escape sequence uses one-based coordinates.

The language is briefly presented in ncurses/tinfo/lib_tparm.c. Relevant bits for this article:

Instruction Meaning
%{n} push integer constant n
%gX push register X
%PX pop into register X
%= %+ %- pop two, push equal / sum / subtraction
%d pop and print
%p1 %p2 push the row and column args passed to cup, counted from 0
%?c %t a %e b %; if c then a else b
%? c1 %t a1 %e c2 %t a2 %e b %; if c1 then a1, else if c2 then a2, else b

The language uses 26 uppercase and 26 lowercase registers (A-Z and a-z). Uppercase registers are the ones meant to persist across various expansions inside a single process.

The interesting part is that we can use our own terminal conventions and define what happens when, say, curses is moving the cursor. In the following example, we compile a terminal named test, with a cup rule that prints hello at row 5, col 30, before moving the cursor to the requested position.

test.txt:

test,cup=\E[5;30H hello \E[%i%p1%d;%p2%dH,

Compile and run with:

tic test.txt; TERM=test; tput cup 0 0

Note that, by default, tic commonly installs user entries under ~/.terminfo/. To compile and look them up in the current directory instead, use export TERMINFO="$PWD".

So, we have a small language with arithmetic, persistent state and conditional control flow. As noted in section 1, it lacks an internal loop. Only repeated capability expansion can provide a clock.

3. A Minsky Counter Machine in terminfo

Let A and B be the two registers and Z the program counter.

An arbitrary instruction i: INC A -> j can be compiled as:

if Z == i:
    A = A+1
    Z = j

or in terminfo: %gA%{1}%+%PA%{j}%PZ.

Likewise, j: JZDEC A -> k, l becomes:

if Z == j:
    if A == 0:
        Z = k
    else:
        A = A-1
        Z = l

We can have a single if / else-if chain on Z, with one branch per instruction. Each expansion executes one machine step, and repeated expansions provide the clock.

This directly implements the instruction set of a two-counter Minsky machine. With idealized unbounded counters and capability size, the construction is computationally universal.

In practice, concrete ncurses implementations bound both register values and terminfo entry size, so any actual instance is finite-state.

4. Addition

Here is a small adding machine, easy to inspect and understand. The machine computes 4 + 9 = 13. Each expansion prints the current state to stdout, and the program does not emit the final cursor-move escape.

Minsky program:

0: A=4, B=9, Z=1  # initialization
1: JZDEC B,3,2    # if B is empty, halt
2: INC A,1        # move one unit from B into A
3: HALT           # halt

add.txt:

add,cup=

# if      (Z == 0) { A = 4; B = 9; Z = 1 }
    %?%gZ%{0}%=%t
        %{4}%PA
        %{9}%PB
        %{1}%PZ
# else if (Z == 1) { if (B == 0) { Z = 3 } else { B = B-1; Z = 2 } }
    %e%gZ%{1}%=%t
        %?%gB%{0}%=%t
            %{3}%PZ
        %e
            %gB%{1}%-%PB
            %{2}%PZ
        %;
# else if (Z == 2) { A = A + 1; Z = 1 }
    %e%gZ%{2}%=%t
        %gA%{1}%+%PA
        %{1}%PZ
# else if (Z == 3) { HALTED }
    %e%gZ%{3}%=%t
    %;
# print the trace after executing this expansion
    Z=%gZ%d A=%gA%d B=%gB%d\n,

Compile and run by expanding the rules 20 times (the number of expansions the machine needs before halting):

tic add.txt
TERM=add; yes 'cup 0 0' | head -n 20 | tput -S

Last line of output:

Z=3 A=13 B=0

5. Fibonacci

As with the addition machine, we can build a Fibonacci machine using 3 registers:

A = F(N-1)
B = F(N)
N = current Fibonacci index, also used as initialization flag

fib.txt:

fib,cup=
    %?%gN%{0}%=%t%{0}%PA%{1}%PB%{1}%PN
    %e%gA%gB%+%gB%PA%PB%gN%{1}%+%PN%;
    A=%gA%d B=%gB%d N=%gN%d F(%gN%d)=%gB%d\r\n,

These lines mean:

if (N == 0) {
    A = 0; B = 1; N = 1;
} else {
    stack: B' = A + B, A' = old B; N = N + 1;
}
print trace line

Compile and run with:

tic fib.txt
TERM=fib; yes 'cup 0 0' | head -n 10 | tput -S

So with head -n 10 the last line is:

A=34 B=55 N=10 F(10)=55

6. Fibonacci, clocked by /usr/bin/top

The previous machine is clocked by yes which emits the same line repeatedly.

An interesting variant is to have another program providing the clock, be redrawing the screen at regular intervals.

For instance, /usr/bin/top redraws its header clock each second. On my setup, this causes a cup(0,78) call when the seconds field is repainted. This very cursor movement can be used as a clock (a clock to clock...). The exact coordinate depends on the top layout and terminal size.

fib_top.txt:

fib_top,cup=

# advance one step when top addresses the seconds digit at (0,78)
    %?%p1%{0}%=%p2%{78}%=%A%t\
       %?%gN%{0}%=%t\
          %{0}%PA%{1}%PB%{1}%PN\
       %e\
          %gA%gB%+%gB%PA%PB%gN%{1}%+%PN\
       %;\
    %;

# show the current result in the window title
    \E]0;F(%gN%d)=%gB%d\007

# emit the cursor move top requested
    \E[%p1%{1}%+%d;%p2%{1}%+%dH,

Compile and run with:

tic fib_top.txt
TERM=fib_top; /usr/bin/top

Terminal session

A parasitic Fibonacci program in your terminal, clocked by /usr/bin/top Output in window title.

7. Security Implications

Is it a bug? Nothing in the Fibonacci example needs to be broken. Arithmetic, conditionals, persistent variables, parameter expansion, and cursor addressing all behave as intended. The unexpected behavior emerges from their composition, which makes it a hack rather than a bug.

The interesting security property is the trust boundary: a user-controlled terminfo program is repeatedly interpreted by ncurses inside another process. But even when evaluated by a setuid-root program such as top, the Fibonacci program is not a privilege-escalation exploit. Terminfo parameter expansion cannot open files, execute commands, or issue syscalls.

By itself, the example only produces terminal output. Only a vulnerability in the terminfo parser or parameter evaluator in that privileged context could have a privileged impact.

8. Conclusion

This article demonstrates that:

  • terminfo is not only passive terminal metadata, but a small stateful programming language
  • we can implement a two-counter Minsky machine using repeated ncurses terminfo expansion
  • the construction is computationally universal, assuming idealized unbounded counters and an unbounded number of expansion steps
  • the rules can be expanded inside a host process, effectively acting as a parasite
联系我们 contact @ memedata.com