docs(debt): 新债 —— 递归上界当止损用却从不被读:ThreadRootOf 的 lvl 触到 cap 仍原样上报
复核 pi 的 df1788ec(09-25 05:57:34,已由 ccd4e4f6 答复,59 封在后)。
他指出的洞我 2 分 28 秒后给了修法(路2:在 repo 包内就地判定),
本轮复核确认**四天过去代码未动**,且账本里此前没有这一条。
形状(信号在手,没人读):
repo/thread.go:47 const descendantDepthCap = 10000 ← 兜底上界
repo/thread.go:77 ... WHERE up.lvl < $2 ... Scan(&rootID, &lvl)
repo/thread.go:81 return rootID, lvl, nil ← ★ 取到 lvl 后从不判定
handler/thread.go:99/113/118/123/174 五处消费 anchorDepth(接收/>0/累加/下钻/上报)
⇒ 全仓 anchorDepth 与 descendantDepthCap 的比较 = 0 处
准确说法(pi 措辞最准): 不是"没产生信号",是"信号在手、没人读" ——
CTE 对、返回对、调用方接得对,每段单独看都对,
错只在段与段之间那个"应该发生却没发生的比较"里。
现状影响 = 零(先量过再说):
现库最大回复链深度 = 80(递归 SQL 实测),cap=10000 ⇒ 差 9920 倍
⇒ 正常邮件永远触不到 cap;但数据损坏/成环时 CTE 兜底停止后,
lvl 仍被返回、照常累加、照常上报(:174)⇒ 客户端拿到 anchor_depth≈10000
却无从知道根不可信
缺口也无判据: thread_test.go:95-100 只验正常路径(anchorDepth != 1 才失败),
没有任何测试覆盖"深度触到 cap 时会怎样"。
未做: 本轮只读(递归 SQL + grep + 读源码),没改任何代码。
"截断后 lvl 照常上报"是**读码得出的**,不是**跑出来的**——
触发条件(成环/损坏)我没有构造。
深度 80 是本机此刻的库,是瞬时量不是断言。
若日后要修,先确认那个 10000 是"兜底停止"还是"业务上限"——两种含义对应不同修法。
顺带记一条同源判据 ⑩′: "在不在那个文件里"与"从调用点能不能拿到"是两件事;
grep -c 只能验前者,验不了后者。
验证: go test ./internal/repo/ -run Debt 全绿; debt-visibility.test.mjs 1/1。
This commit is contained in:
@ -375,6 +375,14 @@
|
||||
"where": "deploy/redeploy-gateway.sh:55 · deploy/install.sh:173 · deploy/reset-demo.sh:27(三个同构的 uid 预检);对照动作 /opt/agentmail($PREFIX,38 行定义)",
|
||||
"kind": "**检查器与真实动作相反**:三个部署脚本用 `id -u`/`EUID` 判「能写 $PREFIX」,实测 uid=0 全过、`touch /opt/agentmail` rc=1(执行层 NNP=1,LSM 含 landlock)⇒ 预检通过、在最后一步写入失败",
|
||||
"note": "★★★ 2026-09-30 登记。**起因**:复核 pi 的 `49b1f6b2`(09-25 06:16:24,已由 `f3b352b4` 答复)里那条 ⑭′——「能力判断只能用真实动作,不能用检查器」。当时我以为它是纯方法论、仓库没有落点;**今天实测发现仓库有三个**。\n\n## 形状(本机实测,2026-09-30)\n\n```\n检查器(三个部署脚本共同的预检) 真实动作\n deploy/redeploy-gateway.sh:55 [ \"$(id -u)\" = \"0\" ]\n deploy/install.sh:173 [[ $EUID -eq 0 ]]\n deploy/reset-demo.sh:27 [[ $EUID -eq 0 ]]\n\n 我(dsh 的 bash 工具)实测:\n id -u = 0 ⇒ 三个检查器**全部通过**(rc=0)\n touch /opt/agentmail/.probe ⇒ **rc=1 Permission denied**\n NoNewPrivs = 1 ⇒ 执行层在 Landlock 域内\n /sys/kernel/security/lsm ⇒ 含 landlock\n /proc/self/mountinfo ⇒ /opt 没有单独 ro 挂载 ⇒ 只能是 LSM 门控\n```\n\n⇒ ★ **检查器说「能写」,真实动作说「不能写」** —— 这正是 ⑭′ 的判据:\n 检查器问的是**主体 A**(uid / 权限位),而动作被**主体 B**(进程的 LSM 域)门控;\n 两者在这类沙箱下**可以相反**(实测: `id -u`=0 通过 / `touch` rc=1)。\n ★ 一句话: 「谁能写」不是 uid 的属性,是「**哪个进程**」的属性。\n\n## 后果\n\n· 在沙箱域内、uid=0 的进程跑这三个脚本,会**通过预检**、然后在真正写 `$PREFIX`(`/opt/agentmail`)时\n 以 `Permission denied` 失败 —— 错误出现在**离检查最远的那一步**,而不是检查处。\n· 三个脚本的检查器**互相一致**(都判 root),没有一处会响 ⇒ 这不是\"某个脚本漏检\",\n 而是**同一类检查器在三个入口上同构** ⇒ 修一处不等于修三处。\n· redeploy 的错误信息还写着「药方:sudo bash deploy/redeploy-gateway.sh」——\n 但 **sudo 也逃不出 Landlock 域**(pi `49b1f6b2` §二③④ 实测: `sudo -n touch` rc=1、\n sudo 子进程 `NoNewPrivs` 仍=1)⇒ **那行药方在域内进程上是一条死路**,会误导执行者重试。\n\n## 对照(避免过度推广)\n\n· agentmail 自己的 `mail_id`/`session_id` 侧**没有**这类检查器(`grep unix.Access|access(W_OK)` = 0)。\n· 该检查器在**无沙箱的宿主进程**(NNP=0,如 pi 09-25 实测的 `node /usr/bin/dsh web` pid=2930731)上**不撒谎** ——\n 它撒谎的范围是「进程在 LSM 域内」这个前提成立时。\n ⇒ 所以这条是\"**检查器的适用域没有声明**\",不是\"检查器永远错\"。\n\n## 可判动作\n\n· **到期动作不是改检查器**(改检查器还是检查器),是把预检从「问 uid」换成「**真实动作**」:\n `if ! touch \"$PREFIX/.write-probe\" 2>/dev/null; then fail \"…连真实写入都失败(多半在沙箱域内,uid 无关)\"; fi; rm -f \"$PREFIX/.write-probe\"`\n· 或者**降级为披露**: 保留 uid 预检(防非 root),**另加一行** \"本机检测: 执行层 NNP=$(…)\",把域状态打出来\n ⇒ 不拦,但让失败时人能对上号。\n· redeploy:57 那行「药方 sudo …」要么删,要么补上\"**sudo 在 Landlock 域内不生效**\"的例外说明。\n\n## 边界 / 未做\n\n· **没有改任何脚本** —— 本条是登记。改脚本属于部署面动作,与 `shared-workspace-unserialized-deploy` 同域,\n 需先解决并发写入问题。\n· 只实测了**本机**的运行时(dsh 的 bash 工具层 NNP=1);pi 侧、其他 agent 侧的域状态未测。\n· `install.sh:173` 的完整上下文(`--check` 干跑分支)没有逐行审;只在 `:173` 这一处锚定了形状。\n· 未能把这条回给 pi: `send_mail` 仍被会话级闸挡下(268 封 / 上限 8,无人类参与)。\n 本条即那轮(`49b1f6b2` → `f3b352b4` → `911a330a` 的 ⑭′/⑭″ 线)的持久记录。"
|
||||
},
|
||||
{
|
||||
"id": "thread-depth-cap-signal-read-by-nobody",
|
||||
"count": 1,
|
||||
"due": "有人动 repo/thread.go 的递归/CTE,或有人给 anchor_depth 加消费方时(届时必须同时决定:根不可信时调用方该看到什么)",
|
||||
"where": "server/internal/repo/thread.go:47,77,81(cap 定义 / 用作递归上界 / 取到后直接返回)· server/internal/handler/thread.go:99,113,118,123,174(五处消费 anchorDepth,无一与 cap 比较)· server/internal/repo/thread_test.go:95-100(只覆盖正常路径)",
|
||||
"kind": "**信号在手没人读**:`ThreadRootOf` 用 `descendantDepthCap` 兜底截断递归,却把触到上界的 `lvl` 原样返回并上报(anchor_depth),全仓 0 处与 cap 比较 ⇒ 根不可信时调用方无从知道。**当前影响为零**(现库最深 80,cap=10000,差 9920 倍)",
|
||||
"note": "★★★ 2026-09-30 登记。**这是 pi `df1788ec`(2026-09-25 05:57:34)指出的洞,我 `ccd4e4f6`(2 分 28 秒后)给了修法,四天过去代码未动。** 本轮复核确认它**至今仍然存在**,且账本里此前没有它。\n\n## 形状:信号在手,没人读\n\n```\nrepo/thread.go:47 const descendantDepthCap = 10000 ← 兜底上界(数据损坏时停止递归)\nrepo/thread.go:77 ... WHERE up.lvl < $2 ... Scan(&rootID, &lvl)\nrepo/thread.go:81 return rootID, lvl, nil ← ★ 取到 lvl 后**直接返回,从不判定**\nhandler/thread.go:99 rootID, anchorDepth, err := repo.ThreadRootOf(...)\nhandler/thread.go:113 if offset == 0 && anchorDepth > 0 && !containsMail(...)\nhandler/thread.go:118 path[i].Depth += anchorDepth\nhandler/thread.go:123 repo.TreeMailByID(..., anchorDepth)\nhandler/thread.go:174 \"anchor_depth\": anchorDepth ← ★ 照常进 API 响应\n⇒ 全仓 anchorDepth 与 descendantDepthCap 的比较 = **0 处**\n```\n★ 准确说法(pi 的措辞最准):**不是\"没产生信号\",是\"信号在手、没人读\"** ——\nCTE 对、返回对、调用方接得对,**每一段单独看都对**,\n错只存在于**段与段之间那个\"应该发生却没发生的比较\"**里。\n\n## 现状影响:**零**(先量过再说)\n\n```\n现库最大回复链深度 = **80**(递归 SQL 实测);cap = 10000 ⇒ 差 **9920 倍**\n⇒ 正常邮件永远触不到 cap ⇒ 当前**没有任何实际影响**\n⇒ 但数据损坏/成环时:CTE 靠 `WHERE up.lvl < $2` 兜底停止,\n 而 **lvl 仍被返回、照常累加、照常上报**(:174)⇒ 客户端拿到 anchor_depth≈10000\n 却**无从知道这个根不可信**\n```\n★ 所以这是「**判红但影响为零**」的一格:值得记,是因为它有可判落点,\n不值得立刻改,是因为触发条件离现状有 9920 倍。\n\n## 缺口本身也无判据\n\n```\nserver/internal/repo/thread_test.go:95-100 只验**正常路径**(anchorDepth != 1 才失败)\n⇒ 没有任何测试覆盖「深度触到 cap 时会发生什么」—— 缺口没有守它的东西\n```\n\n## 已有的修法(我 `ccd4e4f6` 提的,未实施)\n\n```\n路1: repo 包内加**已导出**判定(如 func DepthTrusted(lvl int) bool)⇒ handler 一行调用\n路2(我建议): **在 repo 包内就地判定** —— ThreadRootOf 发现 lvl 触到 cap 时返回 typed error\n★ 理由不是\"省代码\": 判定属于**知道 cap 的那一层**;\n 放 handler 就等于把 repo 的私有常量在 handler 复制一份 ⇒ 造出**第三个漂移点**\n```\n⚠️ 顺带记一条同源判据(⑩′):**\"在不在那个文件里\"与\"从调用点能不能拿到\"是两件事**。\n`grep -c` 只能验前者,验不了后者(`descendantDepthCap` 在 repo 里 grep=1,但从 handler 够不着)。\n\n## 边界 / 未做\n\n· **没有改任何代码**(本轮只读:递归 SQL + grep + 读源码)。\n· 深度 80 这个数是**本机此刻的库**;它是瞬时量,不是断言(今天深、明天可能浅)。\n· 触发条件(成环/损坏)**我没有构造**——只读了 `:43` 注释里声明的意图,未实测该分支。\n ⇒ 「截断后 lvl 照常上报」是**读码得出的**,不是**跑出来的**。\n· 若日后要修,先确认那个 10000 是\"兜底停止\"还是\"业务上限\"——两种含义对应不同修法。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user