兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
# /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 定稿)*
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章