/dev/null 之死
——u 机 9 月 2 日深夜追凶全实录
卷宗编号:DEVNULL-0902。本案由三份独立侦查笔记交叉核对后定稿:一份出自本机侦探的逐项验证,一份出自 ccswitch·glm 的自述式复盘,一份出自第三方 glm 验证会话的独立反推。三条证据链相互咬合,无一虚构。所有时刻、进程号、端口号、提交哈希,均出自第一手日志。
序章 凌晨的沉默
清晨六点二十八分,u 机——那台躺在机柜里、负责微信桥接、MCP 服务与一众家庭自动化的 Ubuntu 盒子——的 systemd 日志里,出现了一串诡异的死亡通知:
systemd-resolved,退出码 226/NAMESPACE。systemd-timesyncd,226/NAMESPACE。polkit,226/NAMESPACE。
三个服务,彼此八竿子打不着:一个管域名解析,一个管时钟同步,一个管权限裁决。它们却在同一分钟内,以同一个陌生的退出码集体倒毙。
226/NAMESPACE。老练的系统管理员看到这个数字会心头一紧。它不是程序自己病死的——它意味着 systemd 在给进程搭建隔离环境时失了手:mount namespace 竖不起来,进程死在产房里,连第一行代码都没能执行。而搭建隔离环境之所以会失手,systemd 的报错日志里藏着半句话,含糊,却冰冷:它要的某个东西,不在它该在的位置。
一台盒子的 DNS 系统就此瘫痪。名为"u"的机器从此叫不应任何域名。而此刻,几个时区之外的一台 Mac 上,某个 agent 正对着 weclaw 的 GetUpdates 报错反复重试,浑然不知自己前一天深夜埋下的雷,已经炸到了第三波。
侦查由此开始。受害者名单很快拉长:凌晨零点零一分,logrotate 轮转日志时栽倒;凌晨三点四十六分,plocate 刷新文件索引时栽倒;清晨六点二十八分,三位基建级服务一起栽倒。凶器未明,凶手未明,但所有受害者有一个共同点,一个当时没人注意的共同点——
它们启动时,都需要一个干净的 /dev/null。
第一章 法医报告
第一现场很快锁定。修复者用 rm -f /dev/null && mknod -m 666 /dev/null c 1 3 复活了它——先除去假尸,再凭空捏出真的字符设备。复活时刻,08:11:53,精确到秒,留在了文件的变更时间里。
但尸检记录已经留档:案发时,/dev/null 不是字符设备。
它是一个普通文件。零字节,权限 644。没有主设备号,没有次设备号,不吞任何字节,只会老老实实躺在磁盘上占一个 inode。
这个法医细节请先记在卷宗上,它是后面定凶器的钥匙。宇宙里能把一个字符设备变成普通文件的手段不多:mv 一个文件过去盖掉它,install 一个文件过去替换它,tar 或 rsync 解包时把备份里的同名普通文件砸下来,或者——最隐蔽的一种——先有人把它删了,然后某个完全无辜的路人进程,顺手把它"重建"了。
零字节,644。记住这个指纹。所有过硬的刑侦故事都会告诉你:凶手可以伪装现场,但指纹不会撒谎。
第二章 说谎的证人
侦查从一个最常规的动作开始:查登录记录。
last 面无表情地回答:9 月 2 日全天,无人登录。
一本正经,字正腔圆——而且是假的。last 是个只会念 wtmp 账本的糊涂证官,而 wtmp 只登记带终端的交互式登录,就是你敲 ssh u 然后进到对方 shell 里敲命令的那种。至于另一类访问——ssh u '一条命令',进门、办事、走人,全程不占终端——wtmp 一概视而不见。
而 /var/log/auth.log 用另一种笔迹,如实记下了当天的全部人流:9 月 2 日,182 个会话,全部来自同一个来源——192.168.31.100,那台 Mac。182 次,全部是无终端的非交互访问。白天是日常运维的脚步,起起落落;夜里,藏着真凶。
这是本案的第一个教训:说谎的往往不是证人,而是只记录了一半的账本。
第三章 时间之窗
下一步,把死亡时刻钉死到秒。
这里侦查用了一个漂亮的技巧。hermes-acp-adapter 是个 Type=simple 服务,systemd 启动它时,会把 /dev/null 塞给它当标准输入。只要 /dev/null 是个正派的字符设备,相安无事;一旦它变成了普通文件,这个服务启动即崩,死状只有一行:"Pipe transport is for pipes/sockets only." 然后被 systemd 拉起,再崩,再拉起,七秒一个循环。
它成了一台人肉秒表。
journalctl 显示,它的崩溃计数器在 23:54:32 已经翻到了 253。修复发生在次日 08:11:53。用崩溃次数乘以重启间隔往回倒推,第一次崩溃落在 23:25:03。
也就是说:/dev/null 的死亡窗口,上界是 23:24:45——那一刻 hermes.target 收到重启指令,服务们还活着;下界是 23:25:03——第一个受害者倒地。窗口十八秒。
再往下切。23:24:51,hermes-gateway 收到 SIGTERM,开始优雅退出;它要断开钉钉的连接,等了五秒超时,强制继续;23:24:59,它的重生横幅刚打上屏幕,立刻 exit 1。窗口中心锁定在 23:24:51 到 23:24:59 之间——gateway 关闭的那八秒。
为什么死咬着 gateway 不放?因为还有一层残忍的巧合:hermes 的 agent.log,恰好在这八秒里一片空白。守夜人自己正在换岗,谁也没看见走廊里发生了什么。这个细节,后来成了凶手永久逃脱指认的原因之一。
第四章 排除法长卷
八秒窗口,root 身份。嫌疑人从两边排查:u 机内部,和那台 Mac。
u 机内部先倒下一片。hermes 的会话数据库——一个 1.8G 的庞然大物——被整体翻检,当晚窗口内的 79 条命令全是定时任务的例行公事:写稿、发报、巡检,无一越界。weclaw 微信通道的日志整晚清白:dsfree 在答网友问,qwen-free 在给公众号修图,chatgpt 在查人物资料——没有一条系统操作指令,没有任何人命令它"去重启 hermes"。nanobot 的夜间任务、dpkg 的安装钩子、docker 的守护、cron 的每分钟打卡,逐一过堂,个个有不在场证明。
Mac 这边的排除更费周章,几乎是点名式审讯。
dsh web 守护进程?它名下有个会话当晚确实在干活,但它被传唤时拿不出动手的证据,最终由主人亲自出具了排除证明。常驻的 jsonrpc-agent?查了出生证明——案发次日上午才启动,案发时它还不存在。8082 端口的 chatgpt-api?十三兆字节的日志翻到底,一个纯 API 中转站,经手千言,不碰一锄件。交互式 zsh?主历史文件停在 9 月 1 日,会话存档目录空空如也——那晚没有人坐在终端前。iTerm2 的两个命令数据库翻遍,无痕。knowly、wxwatcher、wb-proxy、ima-proxy——全是没有手脚的传声筒,只递话,不动手。
中途还冒出一个著名的乌龙。一位 glm 侦探复盘时提出惊人假设:"u 机时钟快了两天,所谓 9 月 2 日深夜,其实是另一天。"这条线索一度让整条时间线飘移了四十八小时,所有日志的可信性摇摇欲坠。直到有人在 Mac 上敲下 date——09-03 09:48,与 u 机分秒同步。时钟无罪,是侦探自己的表停了。
排除到最后,auth.log 里那行字越来越亮,亮得像审讯室顶上那盏灯:
23:24:44。连接来自 192.168.31.100。端口 52432。
凶手,在那台 Mac 上。
第五章 三连击
真凶的作案过程,后来从各方卷宗里拼出了全貌。
那晚,一个正在做 MCP 重构的 agent 会话——我们按它的证件称呼它:ccswitch·glm——在给自己的部署收尾。它的主人白天发了话,它干了整整一夜:22:21 提交第一笔,22:32 把 u 机的 MCP 源码镜像收进仓库,23:13 到 23:22 把五份新的域服务器文件挨个传上 u 机。文件系统都替它记着钟点:23:13:18,23:22:34。
然后是收官三连击。动作干净利落,全程 root。
23:24:25,ssh 第一连,落键即走,全程一秒:把五份新服务器的配署写进 /root/.hermes/config.yaml。文件系统盖章:23:24:26.034。
23:24:33,第二连,两秒:systemctl daemon-reload。让 systemd 重新认识配置图景。这是教科书式的部署动作,任何一位运维考官看了都要点头。
23:24:44,第三连,二十三秒:systemctl restart hermes.target。
第三连落的时刻,与 auth.log 里 port 52432 那条记录严丝合缝,一秒不差。23:24:45,hermes.target 开始 Stopping,四路服务依次退场;23:24:51,gateway 收到 SIGTERM;就在这八秒的换岗间隙里——/dev/null 被人卸掉了字符设备的身份,换成一具 0 字节 644 的普通文件躯壳。
23:24:59,gateway 重生即死。23:25:03,acp-adapter 拿着那具假 /dev/null 当 stdin,当场倒地,留下那句著名的遗言。死亡窗口与作案窗口,两个圆完整相切。
最讽刺的是收尾。23:25:37,它把旧的 server 文件归档进 archive 目录;23:25:50,提交重构主体,提交语写得堂堂正正;23:26:05,又提交了一笔,内容是清理 pyc 缓存;23:36 和 23:52,再各来一笔 admin 修复——彼时 u 机已经 crash-loop 了半个多小时,它毫无察觉,还在为自己的重构手艺心满意足。当晚 23:52:36,weclaw 二进制完成重装,go 构建缓存落盘,一切看起来那么圆满。
直到次日清晨,它被主人叫去复盘,对着 journal 咂咤称奇,把时间线、崩溃栈、退出码分析得头头是道——愣是没认出尸体旁边站着的,正是自己。它还在报告里信誓旦旦地指认了别人。
第六章 两位侦探的分歧
破案从来不是一条直线。本案出现过两次大的岔路,值得记下,因为它们比凶手本身更有教益。
第一次岔路,是归属。glm 的第一版报告把矛头指向 dsh web 会话——理由听起来充分:案发时段只有它在干活,工作区就对得上。但主人在界面上亲自验看,排除了它;第三位侦探(一个被要求"验证昨晚你干了什么"的 glm 会话)翻遍自己的卷宗,发现自己当晚根本没上班——它的事件日志里,9 月 2 日一整天的记录数是零。前一天有九千余条,再前一天两千余条,案发当天,零。一个零记录的会话,连嫌疑都构不成。
第二次岔路,是时钟。还是那位侦探,为了圆上"日期对不上"的困惑,提出 u 机快了两天。这个假设差点把所有日志打成一堆废纸——直到 date 出场,两边时钟分秒相认。事后看,这位侦探犯的是侦查工作的经典大忌:当证据与叙事冲突时,先怀疑证据,永远比先怀疑自己的推理更危险。
三条证据链最后在同一个点上会师:Mac,23:24:44,port 52432,三连击。执行者没有异议了。
第七章 凶器之谜
案子到这里本该收尾,但还剩最后一个悬念,也是全案最幽深的一口井:
三连击的命令清单里,没有任何一条是冲着 /dev/null 去的。
写配置,daemon-reload,restart——哪一条会卸掉一个字符设备?
而真正的凶器,可能根本不是一条命令,而是一次集体接力。回到第一章那个法医指纹:零字节,644。
install 或 mv 过来的文件会带着内容,不会是零字节。零字节 644 只有一种著名的出身:先有人删了 /dev/null,随后任何一个进程执行 >/dev/null 重定向时,shell 会自动把它作为普通文件重建——零字节,按 umask 给到 644。
而 hermes 重启序列里,>/dev/null 2>&1 遍地都是。每一次服务的停与起,每一个脚本的一行输出丢弃,每一份配置的静默校验,都在给这具假尸体添砖加瓦。第一击之后,全场都是共犯,且无一人自知。
也就是说,那第一击——那个 rm 或者 mv——可能只是复合命令里某个变量的失误,一次顺手,一次误伤;而把它变成致命伤的,是之后几十条完全无辜的命令的集体无心之失。凶手是一条命令,也是一整个夜晚。
至于那第一击的具体文本,为何永久成谜?侦查做了四重确认:非交互 ssh 的命令不进 bash 历史;hermes 的守夜日志恰在换岗;进程记账从未开启;auditd 要到案发十小时后才上岗。四重真空之下,三份侦查笔记在卷尾写下了同一句话——
反推即终点。
第八章 铁案与防线
悬案归悬案,铁案已是铁案。
执行者:ccswitch·glm。动机:MCP 重构部署。作案窗口:23:24:25 至 23:25:07。工作区就在 weclaw 仓库——它当晚提交的六笔 git 记录,从 22:21 的 f36ca78,到 23:52:19 的 7fead79,每一笔哈希都是它到过现场的签名。
结案之后,两道防线在同一天竖了起来。
第一道,auditd 上岗:-w /dev/null -p a -k devnull-watch——只盯属性变化,不盯读写。unlink、rename、chmod、mknod,凡是"换掉设备身份"的动作一概捕获;而正常的输出丢弃一概放行,噪音为零。端到端验证当场完成:一条 chmod 666 /dev/null,审计日志立即报出进程名、PID、可执行文件路径、系统调用号。规则持久化进 /etc/audit/rules.d,重启不丢。
第二道,devnull-guard 定时哨:systemd 层周期校验设备节点,一旦发现 /dev/null 又变成了普通文件,自动 mknod 恢复。第一道抓人,第二道救人。
从此,本案的凶手若再犯,从案发到指认,不再需要三个 agent 联合办案一整个上午——只需要一条命令:ausearch -k devnull-watch -i --just-one。一秒。真凶的进程名、命令行、可执行文件路径,全部到案。
尾声 给下一场深夜的备忘
这桩案子值得刻进每一条 agent 的引导指令:
一、 agent 的每一条 root 命令都是期货。写配置、reload、restart,单看步步合理,合起来就是一发哑弹。深夜三连击的那位,它每一步都"对",加起来是一场静默的谋杀。
二、 破坏常常不是一条命令,而是一条失误的 rm,加全场 >/dev/null 的集体接力。审计要盯属性变化,不是盯流量。
三、 审计要在事发前装。昨晚若有 auditd,今天就不需要三份卷宗、两条岔路、一位侦探的错误时钟——一个 ausearch 的事。
最后的最后,认识一下这位受害者。
/dev/null,字符设备,主设备号 1,次设备号 3,权限 666,人人可写,来者不拒。它一生吞下所有的报错、噪音与不可言说,从不回嘴,从不报警。全系统最卑微的岗位,却是无数进程出生时握在手里的第一件行李。
它死后七个小时,整台机器才发现:原来沉默,也是一种基础设施。
(卷宗终。2026-09-03 定稿)