From 9cea5140102c0eb04bd9aaaa723374eaa68de87b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 22 Sep 2026 21:53:05 -0400 Subject: [PATCH] =?UTF-8?q?[docs]=20(P7):=20ph-f.md=20=C2=A76/=C2=A77=20re?= =?UTF-8?q?cord=20the=20F=20fix=20round=20-=20the=20fingerprint=20refusal?= =?UTF-8?q?=20is=20one-directional=20and=20an=20un-redeployed=20phone=20sh?= =?UTF-8?q?ows=20as=20"no=20Welcome",=20the=20unix=20pairing=20window=20an?= =?UTF-8?q?d=20the=20two=20pre-auth=20exposures=20are=20=C2=A712=20debts,?= =?UTF-8?q?=20the=20token=20minimum=20is=20TCP-only,=20and=20the=20token?= =?UTF-8?q?=20examples=20are=2016+=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ph-f.md: §0 rows F-a..F-d with package-tree SHAs; §2.2 states the asymmetry (new server + old client -> Refuse{WireFingerprint}; old server + new client -> the old AcceptPair closes both after 2000 ms and the client sees `no Welcome ... (rc=6)`, no Refuse frame); §6 the four fix commits with their red-once lines; §6.4 the unix arrival-order pairing (250 ms / 2000 ms) as a local-peer debt with the SELinux assumption named, and the two pre-auth exposures; §7 the gates on 2b2a0ae0 - CONTRACT-P7 §12: the two pre-auth exposures (fork before Hello with a 10 s wait; ValidatePeerHandshake before AuthenticatePeerToken sends the fingerprint and build stamp to an unauthenticated peer), the unix pairing debt, and the fingerprint asymmetry, for PH-7 (5) - PLAN-PH-P34B-P7: PH-7 (3) says the minimum is checked at the TCP listen; PH-8 says "ignored, server-sized" rather than "clamped"; the no-token listen row's remaining-work cell no longer claims the smoke is unreferenced - device-window-1-runbook §2 step 3: the un-redeployed-phone symptom, and where to look first - ROADMAP §6 and peer-loss-probe: `MOBILEGL_IPC_TOKEN=devtoken` (8 bytes, which now exits 72) becomes a 25-byte token in every example --- MobileGL/MG_Remote/CONTRACT-P7.md | 3 +- docs/Disaggregated/ROADMAP.md | 4 +- .../notes/p65/peer-loss-probe.md | 4 +- .../Disaggregated/notes/p7/PLAN-PH-P34B-P7.md | 6 +- .../notes/p7/device-window-1-runbook.md | 7 +- docs/Disaggregated/notes/p7/ph-f.md | 153 ++++++++++++++++-- 6 files changed, 156 insertions(+), 21 deletions(-) diff --git a/MobileGL/MG_Remote/CONTRACT-P7.md b/MobileGL/MG_Remote/CONTRACT-P7.md index 0772f5d7..a67ee48e 100644 --- a/MobileGL/MG_Remote/CONTRACT-P7.md +++ b/MobileGL/MG_Remote/CONTRACT-P7.md @@ -172,4 +172,5 @@ verify **在 wave 3 之前**只在 monolith 臂注册(`MG_IntegrationTest/CMak - **B2 返工留下的记录债(`notes/p7/magma-b2.md` §6,ID-P7-35/37)**:(1) 多重采样颜色 resolve 的第二段 `vkCmdBlitImage` 不裁剪目的矩形(越界 = `VUID-vkCmdBlitImage-dstOffset-00248/00249`),与 monolith 臂共有,两臂一起裁是独立的活;(2) `MagmaWireFatal("multisample-resolve-region")` 对源矩形越出读缓冲的 blit 仍带走会话——GL 说那些像素未定义而非错误,应 decline 或钳制;(3) 1 → N 等矩形 decline 拒绝的是合法形状(18.3.1 样本复制),Vulkan 无对应命令、monolith 的可观测本身是 VU 违规;(4) tcp 车道带不了 server 端测试旋钮——六条 knob 条目只在 split + spawn,`spawn_lane_parity.py` 的 `MAGMA_SERVER_ENV_KNOB_NO_TCP` 具名;收回要 `tcp_server_fixture.py` 接受每用例 server 环境;(5) X 镜像与跨格式 Y 镜像的深 / 模板 resolve 以规则 I(a) 具名 decline(合法 blit);跨格式那条在 renderbuffer 与 texture 的 D24S8 映射到不同 `VkFormat` 的设备上会让 `MsFlip.` 变红;(6) **monolith `DirectVulkan` 臂的翻转 / 缩放多重采样深度 blit**:翻转被拒(目的地保持清屏值、无错误)、缩放被执行(GL 说 `INVALID_OPERATION`)——wire 与 monolith 的分歧,wire 臂是更正确的一方,monolith 修正归 P13 / G1;(7) **Espryt(DirectGLES)的翻转 / 缩放多重采样深度 resolve**:翻转什么也不写且不报错、缩放不报错——P3b / P4b 债;用例在这两类条目上以具名理由 `Skipped`。 - **B3 复审留下的记录债(`notes/p7/magma-b3.md` §7.2,ID-P7-36/37)**:`WaitForFrameSerial` 在 disagg 构建上等的是第一条 `frameSerial >= serial` 的记录,钳制之后若更晚的记录(S2 Present)持有同一序号则地板到不了 `serial`,函数返回 true 而 `IsFrameSerialComplete(serial)` 为 false——唯一调用者 `WaitForTimerQueryResult` 重查就绪,今天退化为一次假超时;修法归 **B4**(只在 `IsFrameSerialComplete` 为真时返回,否则从头重扫,不能 continue)。陈旧注释两处(`CollectAllDeferredReleases` 的「mid-frame drains do not advance m_frameSerial」、`WaitForFrameSerial` 的「floor still advances after one fence wait」)归 B4 一起改。disaggregated 构建的 monolith 臂在中帧 flush 下比 pull 构建更可靠(`OnSubData` 经 `IsResourceBusy` 读同一地板),pull 构建的 `#else` 逐字保留、潜在缺陷由 G1 与 P13 持有。 - **V1 留下的记录债(`notes/p7/verify-split.md` §6,ID-P7-37)**:verify 在 spawn 上需要一个跨进程的比对形状(server 把读到的字段值随回复带回、client 比),门 2 的文本是 `{monolith, split(inproc)}`;server `ReadPixels` 窗口里 pack 半边的字段级盲区很窄(只有恰等于中性 pack 的错推送不可见);DirectGLES × split 上四条 Magma decline 契约用例按名从 verify-split 车道减掉——Espryt 是否 decline 这些形状归 P3b / P4b。 -- **F 片 1–3 之后(`notes/p7/ph-f.md` §4,ID-P7-37)**:`wireFingerprint` 随 `Welcome.dataNonce` / `DataBind` 变更(`0x7d4470f241d23539` → `0x03c4c293a54b3e08`),旧 server 与新 client 按指纹互拒——设备上的 server 必须随下一个 APK 重部署;D11 五处 + PH-2、PH-6、PH-1 (3)(4)、PH-7 (5)(fork 前认证)未做,前置是一个能向 spawn / TCP server 投递畸形记录的对端字节驱动(fuzz 臂 2 的第一块),包 **F2**。 +- **F 片 1–3 之后(`notes/p7/ph-f.md` §4,ID-P7-37)**:`wireFingerprint` 随 `Welcome.dataNonce` / `DataBind` 变更(`0x7d4470f241d23539` → `0x03c4c293a54b3e08`),新 server 对旧 client 回 `Refuse{WireFingerprint}`,旧 server 对新 client **没有 Refuse 帧**(旧 `AcceptPair` 等第二条连接 2000 ms 后关闭,client 只见 `no Welcome … (rc=6)`)——设备上的 server 必须随下一个 APK 重部署;D11 五处 + PH-2、PH-6、PH-1 (3)(4)、PH-7 (5)(fork 前认证)未做,前置是一个能向 spawn / TCP server 投递畸形记录的对端字节驱动(fuzz 臂 2 的第一块),包 **F2**。 +- **Ph 簇 F 的两处预认证暴露**(wave 2-F 之前就在树上,F 修复轮审查核实,归 PH-7 (5)「有界的预认证工作 + 失败退避」,P7 之后):(a) TCP supervisor 对每条连接**先 fork 再读 Hello**(`ServerMain.cpp` `RunSession`),未认证对端每连接可让 server 付出一次 fork + 最长 10 s 的 Hello 等待;(b) `ValidatePeerHandshake` 在 `AuthenticatePeerToken` **之前**跑,`Refuse{ProtocolVersion|WireFingerprint|BuildFingerprint}` 把本端 wire 指纹与 build stamp 送给了未认证的对端。同一轮记下的本地对端债:unix 端点仍按到达顺序配对(`AcceptPair` 250 ms / 2000 ms 窗口),抽象命名空间套接字没有 mode bits,「只有本 app 能连」靠 SELinux 域隔离而非传输层。**指纹不对称**(§10 第 3 条的 `wireFingerprint` 变化):新 server 对旧 client 回 `Refuse{WireFingerprint}`;旧 server 对新 client **没有任何 Refuse 帧**——旧 `AcceptPair` 等第二条连接 2000 ms 后关掉两条,client 只看到 `no Welcome ... (rc=6)`,即设备上的 server 未随本次构建重装。细节 `notes/p7/ph-f.md` §2.2 / §6.4。 diff --git a/docs/Disaggregated/ROADMAP.md b/docs/Disaggregated/ROADMAP.md index 3b3d9b34..ff32e9b7 100644 --- a/docs/Disaggregated/ROADMAP.md +++ b/docs/Disaggregated/ROADMAP.md @@ -128,7 +128,7 @@ server 在手机上**监听**,client 从 WSL **拨出**。理由不是偏好 adb shell ip -f inet addr show wlan0 # 3. 起 server(前台 Service → supervisor 监听) adb shell am start-foreground-service -n top.mobilegl.plugin.trace/top.mobilegl.plugin.MobileGLServerService \ - --es listen tcp://0.0.0.0:40613 --es token devtoken \ + --es listen tcp://0.0.0.0:40613 --es token devtoken-0123456789abcdef \ --es env "MOBILEGL_BACKEND_TYPE=DirectGLES;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;MOBILEGL_LOG_FILE_PATH=/data/data/top.mobilegl.plugin.trace/files/mgl.log" adb logcat -s MobileGL | grep -m1 'listening on tcp://' # 停:adb shell am force-stop top.mobilegl.plugin.trace @@ -139,7 +139,7 @@ adb logcat -s MobileGL | grep -m1 'listening on tcp://' export MOBILEGL_TRANSPORT=spawn # 拓扑:两进程;不 fork,因为下面给了远端 export MOBILEGL_IPC_CONTROL=tcp://:40613 export MOBILEGL_IPC_DATA=stream -export MOBILEGL_IPC_TOKEN=devtoken +export MOBILEGL_IPC_TOKEN=devtoken-0123456789abcdef # ≥16 字节(PH-7 (3)):更短的令牌让 server 以 72 退出而不是监听 export MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 # APK 与本地构建必须同 commit ctest -L integration-tcp-device -j 1 --output-on-failure # -j 1:设备同时只服务一个会话 # retrace(同一环境): diff --git a/docs/Disaggregated/notes/p65/peer-loss-probe.md b/docs/Disaggregated/notes/p65/peer-loss-probe.md index 22be0df8..bcc16659 100644 --- a/docs/Disaggregated/notes/p65/peer-loss-probe.md +++ b/docs/Disaggregated/notes/p65/peer-loss-probe.md @@ -11,7 +11,7 @@ ```bash cmake --build /home/swung/p65-split --target MobileGLPeerLossProbe -j 12 export MOBILEGL_IPC_CONTROL=tcp://PHONE_IP:40613 -export MOBILEGL_IPC_TOKEN=devtoken +export MOBILEGL_IPC_TOKEN=devtoken-0123456789abcdef # ≥16 字节(PH-7 (3)),与 server 端一致 export MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 export MGITEST_PEER_KILL_CMD='adb -s 2f7cbe2e shell run-as top.mobilegl.plugin.trace kill -9 {pid}' /home/swung/p65-split/MobileGL/MG_Test/Wire/MobileGLPeerLossProbe --kill --deadline-ms 10000 @@ -25,7 +25,7 @@ loopback supervisor 的模板可以是 `MGITEST_PEER_KILL_CMD='kill -9 {pid}'` ```bash export MOBILEGL_IPC_CONTROL=tcp://PHONE_IP:40613 -export MOBILEGL_IPC_TOKEN=devtoken +export MOBILEGL_IPC_TOKEN=devtoken-0123456789abcdef # ≥16 字节(PH-7 (3)),与 server 端一致 export MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 run=$(mktemp -d /tmp/p65-wifi.XXXXXX) probe=/home/swung/p65-split/MobileGL/MG_Test/Wire/MobileGLPeerLossProbe diff --git a/docs/Disaggregated/notes/p7/PLAN-PH-P34B-P7.md b/docs/Disaggregated/notes/p7/PLAN-PH-P34B-P7.md index e552a60b..9aaa1c0a 100644 --- a/docs/Disaggregated/notes/p7/PLAN-PH-P34B-P7.md +++ b/docs/Disaggregated/notes/p7/PLAN-PH-P34B-P7.md @@ -29,13 +29,13 @@ P7 收官的关键路径是:**先造仪器(wave 0)→ 真机诊断(wave | PH-5 `ArchiveVector` 上限(`ProgramArtifactsCodec.cpp:309`) | not_started | 必须 | `TakeCount` 带元素大小,`raw*sizeof(T)` 对 `Remaining()` 设界 | 4–8 h | D11 | | D11 第一行 `create_render_state` 的 `Cso.Slot+1` 无界 resize(`PipeApply.cpp:1575`) | not_started | 必须 | 加 `kMGPipeMaxRenderStateCsoSlots`,走同族 `RecordAt(table, slot, limit)` | 2–3 h | D11 | | PH-6 `SEG_EVENT` 不排空对端 | not_started | 必须 | 取 **drop-with-latch** 形状(不依赖 PH-1):`ReserveEventOrBlock`(`ServerSession.cpp:215-263`)两处 BUSY 拒绝改为置 `m_reverseChannelForfeit` + `CountDrop()` + 返回 scratch,`ServerLoop` park 谓词见闩走正常 Stop;`kEventBacklogWaitMs=30000` 改成 `MOBILEGL_IPC_EVENT_WAIT_MS`(默认 2000);先判 `Dead()` 免把对端死亡误报成 30 s 超时。两条单测(stonewall / trickle)。路线图「60 秒」实为 30 s(两轮)且涓流对端毫秒级触发 `:259` | 1.5–2 天 | P7 之后 | -| PH-7 配对 / 认证 | partial ⚠ → **(1)–(4) done**(wave 2-F,`ph-f.md`);(5) 未做 | 必须 | 已有:非 loopback 无令牌拒绝监听(`SocketTransport.cpp:160`)、Hello 带 token、Refuse。欠:(1) **常量时间比较**且两处策略(`ServerSession.cpp:498` / `ServerMain.cpp:93`)合一;(2) 名字 `Refuse{AuthenticationRequired}` 不在 `RefuseCode` 枚举里——用 `Authentication` 或追加枚举,并给 `fatal_census.py` 加 `Refuse{Word}` 词表门;(3) 令牌最小长度 ≥16 字节;(4) **数据面未认证**:`AcceptPair` 按到达顺序配对、从不查 peer(`SocketTransport.cpp:371/:391`)——`Welcome` 带 `dataNonce`,第二连接首帧必须回它;认证挪到 fork 之前;(5) 有界的预认证工作 + 失败退避;(6) OQ-21 裁定(见 §5 ID-P7-3) | (1)(2)(3) 1 天;(4)(5) 2 天 | (1)(2)(3) 与 wave 0 同期;(4)(5) P7 之后 | -| PH-8 尺寸由 server 钳制 | **done**(wave 2-F:单测 + 两进程 smoke `hello_asks_64_gib`) | 必须 | 已陈述;补「client Hello 要 64 GiB 时 server 钳到上限而非分配」的单测 | 0.5 天 | P7 之后 | +| PH-7 配对 / 认证 | partial ⚠ → **(1)–(4) done**(wave 2-F,`ph-f.md`;F 修复轮 `ph-f.md` §6:绑定后关闭 hand-off / 单会话监听者、schema 摘要钉住 revision);(5) 未做 | 必须 | 已有:非 loopback 无令牌拒绝监听(`SocketTransport.cpp:160`)、Hello 带 token、Refuse。欠:(1) **常量时间比较**且两处策略(`ServerSession.cpp:498` / `ServerMain.cpp:93`)合一;(2) 名字 `Refuse{AuthenticationRequired}` 不在 `RefuseCode` 枚举里——用 `Authentication` 或追加枚举,并给 `fatal_census.py` 加 `Refuse{Word}` 词表门;(3) 令牌最小长度 ≥16 字节(在 **TCP** 监听处检查;unix 端点不问);(4) **数据面未认证**:`AcceptPair` 按到达顺序配对、从不查 peer(`SocketTransport.cpp:371/:391`)——`Welcome` 带 `dataNonce`,第二连接首帧必须回它;认证挪到 fork 之前;(5) 有界的预认证工作 + 失败退避;(6) OQ-21 裁定(见 §5 ID-P7-3) | (1)(2)(3) 1 天;(4)(5) 2 天 | (1)(2)(3) 与 wave 0 同期;(4)(5) P7 之后 | +| PH-8 尺寸由 server 钳制 | **done**(wave 2-F:单测 + 两进程 smoke `hello_asks_64_gib`;F 修复轮:超出所授条款的请求记一条 `MGLOG_W`) | 必须 | 已陈述;补「client Hello 要 64 GiB 时 server **忽略请求、按自己的尺寸建段**而非分配」的单测(不是钳制:四个计数根本不参与定尺寸) | 0.5 天 | P7 之后 | | 门:`Session::Fail` 站点普查零新增 | partial ⚠ | 必须 | `scripts/ci/fatal_census.py` 存在且在 CI(`test.yml:2011`),但 (a) `SCAN_ROOT` 只有 `MG_Remote`,server 进程里 ~79 处 abort(DirectGLES.cpp 16、Managers.cpp 12、VkBufferManager.cpp 11、PipeRoute.cpp 7……)看不见;(b) `abort_sites` 记了从不比较;(c) 12 个 family 词无 `.def` 行。改:SCAN_ROOT 扩到 server 镜像(MG_Remote / MG_Pipe / MG_Backend / MG_Impl/Pipe / MG_State),`abort_sites` 变棘轮,补 `.def` 行 | 1 天 | **wave 0**(P7 需要它看见 `@P7` abort) | | 门:fuzz 臂 1 畸形控制帧 | partial | 必须 | 机制已在(`Framing.h:168` latch、`ControlInbox`),欠端到端脚本 `scripts/ci/ph_fuzz_control_frames.py`(五种畸形)+ `ServerMain.cpp:193` 补 `CtrlEnvelopeBufferHasIdentifier` + ctest 车道 | 1.5 天 | P7 之后 | | 门:fuzz 臂 2 超界计数 | not_started | 必须 | = PH-1 (3)(4) + D11 五处 | 见上 | P7 之后 | | 门:fuzz 臂 3 不排空对端 | not_started | 必须 | = PH-6 + 场景 | 见上 | P7 之后 | -| 门:非 loopback 无令牌具名拒绝 | **done**(wave 2-F:smoke 入 CI,SKIP 即红) | 必须 | 见 PH-7 (2):名字不在词表;`tcp_supervisor_smoke.py` **未接 CI**(全仓无引用) | 含在 PH-7 | 同期 | +| 门:非 loopback 无令牌具名拒绝 | **done**(wave 2-F:smoke 入 CI,SKIP 即红) | 必须 | 已了:名字改为词表内的 `Refuse{Authentication}`(PH-7 (2));`tcp_supervisor_smoke.py` 挂为 `TcpLane.SupervisorProtocolControls`(`integration-tcp`),CI 构建钉版 flatc 并以 `junit_tally.py --require-entry-passed` 要求它**跑了且过了** | 含在 PH-7 | 同期 | | OQ-21 令牌够不够 / TLS | 已答(ID-P7-3;wave 2-F 落地) | 必须裁定 | 见 ID-P7-3 | 0 | 本文 | | OQ-24 E1 对照重定义(ID-122) | 无主 | 可选 | 挂到普查门的 S8 (`SessionFaultCount()==0`) 负控设计上 | 0.5 天 | P7 之后 | diff --git a/docs/Disaggregated/notes/p7/device-window-1-runbook.md b/docs/Disaggregated/notes/p7/device-window-1-runbook.md index f9fe3444..31e365eb 100644 --- a/docs/Disaggregated/notes/p7/device-window-1-runbook.md +++ b/docs/Disaggregated/notes/p7/device-window-1-runbook.md @@ -91,7 +91,12 @@ docs/Disaggregated/notes/p7/device-window-1/ 3. **主机侧 TCP 矩阵的制品先备好**(第 7 步用):一份 WSL 的 `libMobileGL.so` + `mobilegl_trace_replay`,以及 `ctest --show-only=json-v1` 的 catalog。两端必须共享同一个 - **显式** `MOBILEGL_BUILD_STAMP`,否则 `MOBILEGL_IPC_REQUIRE_SAME_BUILD=1` 会在握手上拒绝: + **显式** `MOBILEGL_BUILD_STAMP`,否则 `MOBILEGL_IPC_REQUIRE_SAME_BUILD=1` 会在握手上拒绝。 + **设备上的 server 也必须是本次构建**(wave 2-F 起 TCP 握手换成了「一条连接 + `Welcome.dataNonce`」): + 新 APK 的 server 对旧 client 会回 `Refuse{WireFingerprint}`,但**旧 APK 的 server 对新 client 没有任何 + Refuse 帧**——旧 supervisor 等第二条连接 2000 ms 后把两条都关掉(设备日志 `only 1 of 2 connections + arrived`),主机侧只看到 `MG_Remote client: no Welcome ... (rc=6)`;看到这条先回 §3 核对 + `apk-install.txt` 的 `lastUpdateTime`,再怀疑别的: ```bash # [WSL] diff --git a/docs/Disaggregated/notes/p7/ph-f.md b/docs/Disaggregated/notes/p7/ph-f.md index a8588b78..6ff19b51 100644 --- a/docs/Disaggregated/notes/p7/ph-f.md +++ b/docs/Disaggregated/notes/p7/ph-f.md @@ -13,14 +13,20 @@ ## 0. 每片一条提交,每片一次 red-once +下表的 SHA 都是**包树**(分支 `p7/ph-f`)上的;集成者 cherry-pick 到 `pipe` 后以那边的为准。 + | 片 | 项 | 提交 | 状态 | |---|---|---|---| -| 1 | PH-7 (1)(2)(3):常量时间令牌合一、≥16 字节、无令牌只 loopback、smoke 入 CI | `2975a337` | 落地 | +| 1 | PH-7 (1)(2)(3):常量时间令牌合一、≥16 字节(TCP 监听处)、无令牌只 loopback、smoke 入 CI | `2975a337` | 落地 | | 2 | PH-7 (4):`Welcome.dataNonce` 把数据连接绑定到已认证的控制连接 | `596b34fd` | 落地 | -| 3 | PH-8:Hello 要 64 GiB → server 钳到自己的条款且不分配 | `a746417a` | 落地 | +| 3 | PH-8:Hello 要 64 GiB → server 忽略请求、按自己的尺寸建段且不分配 | `a746417a` | 落地 | | 4 | D11 五处上限 + PH-2 | — | **未做**,见 §4 | | 5 | PH-6 drop-with-latch | — | **未做**(顺序在 4 之后) | | 6 | PH-1 (3)(4) | — | **未做**(顺序在 4 之后) | +| F-a | 审查修复 1+4:绑定后关闭 hand-off / 单会话监听者,hand-off 发送不阻塞 | `3370833f` | 落地,见 §6.1 | +| F-b | 审查修复 2:`sha256(protocol.fbs)` 钉住 `MOBILEGL_PROTOCOL_CONTROL_REVISION` | `75f24db0` | 落地,见 §6.2 | +| F-c | 审查 nit 6/8/9/10(代码) | `2b2a0ae0` | 落地,见 §6.3 | +| F-d | 审查 nit 3/5/7/11 + 两条 §12 债(文档) | 本提交 | 落地,见 §6.4 | red-once 原始行汇总在 `~/w7/logs/p7-ph-f-redonce.txt`。 @@ -35,12 +41,14 @@ red-once 原始行汇总在 `~/w7/logs/p7-ph-f-redonce.txt`。 `std::string::operator==`,且在 server 没配令牌时反而要求对端送空令牌。两处比较都在第一个不同字节处返回。 - 新文件 `MG_Remote/Transport/AuthToken.h`:`ConfiguredAuthToken()`(空值 = 未配置)、 - `kMinimumAuthTokenBytes = 16`、`ConstantTimeTokenMatch`(循环长度只取本进程自己的令牌,长度差并入累加器, - 嵌入的 NUL 参与比较)。 + `kMinimumAuthTokenBytes = 16`(**只在 TCP 监听处**问,见下条;unix 端点既不要求令牌也不问长度)、 + `ConstantTimeTokenMatch`(F 修复轮起循环固定 64 字节、两端越界读零,更长的令牌比到最后一字节;长度差并入 + 累加器,嵌入的 NUL 参与比较)。 - `Handshake.h` `AuthenticatePeerToken`:**唯一**的策略函数,`ServerSession::Accept` 与 supervisor 子进程 (`ServerMain::RunSession`)都调它。不配置令牌时不因对端送了令牌而拒绝——策略由监听侧的 loopback 限制负责。 -- `ListenTcp`:配置了但不足 16 字节的令牌**让监听失败**(具名 `Refuse{Authentication} ... minimum is 16`, - 进程退出 72),而不是悄悄退化成 loopback server。令牌值从不进日志。 +- `ListenTcp`:配置了但不足 16 字节的令牌**让 TCP 监听失败**(具名 `Refuse{Authentication} ... minimum is 16`, + 进程退出 72),而不是悄悄退化成 loopback server。令牌值从不进日志。unix 监听者不检查这条:那条路上令牌 + 不是访问控制(§6.4 的本地对端债)。 - `Refuse{Authentication}` 是 `protocol.fbs` `RefuseCode` 的枚举值,`fatal_census.py` 规则 4 的词表门 直接认它;普查前后都是 3 个 refusal 词,无需新增 `.def` 行(Refuse 词不是 Fatal 家族词)。 - CI:`TcpLane.SupervisorProtocolControls` 从 wave 0 起挂在 `integration-tcp` 上却因 CI 没有 flatc 而恒为 @@ -83,9 +91,18 @@ WithATokenShorterThanTheMinimum` 与 `TheTokenComparisonAnswersLengthAndPrefixTh `MOBILEGL_PROTOCOL_CONTROL_REVISION`(`mg_protocol_base.h`,本次 = 1)作为 `AbiFingerprintInputs. ControlSchemaRevision` 混入;`SessionHandshakeTest.TheAbiFingerprintChangesWhenAnyOfItsInputsDoes` 钉住 该输入并加扰动行。实测 `wireFingerprint` 由 `0x7d4470f241d23539` 变为 `0x03c4c293a54b3e08`。 - **设备侧后果**:旧 server 与新 client(或反之)现在以 `Refuse{WireFingerprint}` 拒绝,设备上的 - server 需要随本提交重新部署。 -- 流程(仅 `tcp://`;unix 端点的第二条连接是 SCM_RIGHTS 套接字,保持 `AcceptPair`): + **设备侧后果——两个方向不对称**(审查 nit 3): + - **新 server + 旧 client**:旧 client 仍开两条连接、送旧指纹,新 supervisor 读到 Hello 后以 + `Refuse{WireFingerprint}` 具名拒绝——这是有帧的那个方向。 + - **旧 server + 新 client**(= 手机上的 server **没有**随本提交重装的情形):新 client 只开**一条**连接; + 旧 supervisor 的 `AcceptPair` 等第二条连接 2000 ms,记 `only 1 of 2 connections arrived within ... ms`, + 把两条都关掉、继续监听——**没有子进程、没有读 Hello、没有任何 `Refuse` 帧**。client 侧的症状是 + `MG_Remote client: no Welcome ... (rc=6)`(`MOBILEGL_ERR_TRANSPORT_CLOSED`,对端 EOF),不是一条 + 具名拒绝。看到这条就先查设备上的 APK 是否是本次构建(`device-window-1-runbook.md` §2 第 3 步)。 + - 指纹随 `MOBILEGL_PROTOCOL_CONTROL_REVISION` 变,而 revision 是手改的整数:F 修复轮把 + `sha256(protocol.fbs)` 钉在它旁边(§6.2),改了 schema 不 bump 在单元车道与 flatc-check 里都是红。 +- 流程(仅 `tcp://`;unix 端点的第二条连接是 SCM_RIGHTS 套接字,保持 `AcceptPair`——那是本地对端债, + §6.4): 1. client `SocketTransport::ConnectControl` 只连控制连接,发 Hello; 2. server 子进程在令牌通过后用内核 CSPRNG(`getrandom`,退回 `/dev/urandom`,绝不退回 PRNG)铸 128 位 nonce,`StreamLink::AttachOwnedDeferred` 先建好私有内存(Welcome 要宣告其尺寸)但**不带 fd、不起 @@ -104,7 +121,8 @@ WithATokenShorterThanTheMinimum` 与 `TheTokenComparisonAnswersLengthAndPrefixTh 6. 无活会话时到达的 DataBind 由子进程具名拒绝:`Refuse{Authentication} data connection names no live session`,干净退出 0。 - 250 ms / 2000 ms 的「两连接到达窗口」在 TCP 上不复存在;取而代之的是 nonce 匹配。非 `--serve` 的单会话 - TCP 模式保留监听者,由会话自己 accept 并读 DataBind(`ListenerSource`),比较仍在同一处。 + TCP 模式保留监听者,由会话自己 accept 并读 DataBind(`ListenerSource`),比较仍在同一处;F 修复轮起, + 数据连接绑定后监听者随即关闭(§6.1)。 - 仍然**没有**做的:ID-P7-3 说「认证在 fork 之前完成」——本片的令牌检查与 nonce 铸造仍在 fork 出的会话子 进程里(每会话一进程,不影响绑定的正确性);把 Hello 读取与认证移进 supervisor 属于 PH-7 (5)「有界的预 认证工作 + 失败退避」,未做。 @@ -136,7 +154,9 @@ OnTheSocket` 钉住「恰好一帧」与 16 字节宽度。 ### 3.1 机制 没有改代码:`ServerSession::Accept` 只看 Hello `linkTerms` 的 `dataPlane` / `wireForm`,段按 server 自己的 -`SetSegmentSizes` 建,Welcome 宣告的也是这组。本片把它钉住: +`SetSegmentSizes` 建,Welcome 宣告的也是这组。准确地说这不是「钳制」而是**忽略、按 server 尺寸**:四个计数 +根本没被读去定尺寸(审查 nit 8);F 修复轮起,请求超过所授条款时 `Accept` 记一条 `MGLOG_W`(§6.3),生产 +client 的 Hello 不填这四个数,所以正常会话不会打这条。本片把它钉住: - 单测 `SessionHandshakeTest.AHelloAskingFor64GiBIsClampedToTheServersTermsAndAllocatesNothingOfIt`: Hello 四个窗口各要 64 GiB,Welcome 必须回 server 的 4 KiB 条款,四个 SegmentRef 各 ≤ 64 KiB。 @@ -176,7 +196,7 @@ GREEN 如 3.1 --- -## 5. 门(头 `a746417a`) +## 5. 门(片 1–3 的头 `a746417a`,包树 SHA;F 修复轮的头见 §7) | 门 | 基线 `ec46a550` | 头 | |---|---|---| @@ -189,3 +209,112 @@ GREEN 如 3.1 | integration-spawn | 219 | 219 | | integration-tcp | 222 | 222 | | integration-magma-tcp | 74 | 74 | + +--- + +## 6. F 修复轮:片 1–3 的 fable 审查(裁定 land with fixes) + +审查的 11 条里 should-fix 五条(1–5)、nit 六条(6–11);下面按提交分。每条都有一次 red-once,原始行在 +`~/w7/logs/p7-ph-f-redonce.txt` 的 `F fix round` 段。 + +### 6.1 修复 1 + 4(`3370833f`):绑定之后,数据连接的来源随即关闭 + +**缺陷 1(回归)。** 子进程只在 `BindDataConnection` 里读 hand-off socketpair,数据连接绑定后再也不读;而 +supervisor 的 `RouteWhileBusy` 对此后**每一条** DataBind 仍然 `SendFd` 进去。后果:每个 fd 在子进程的接收 +队列里躺着、对端**没有任何回复**(不是具名拒绝;改前基线对第二条连接还会回 `Refuse{Busy}`),那条 TCP 连接一直 +挂到会话结束;队列到 `net.unix.max_dgram_qlen`(手机内核默认 10;本机 512,但发送端 sndbuf 在 ~288 个 +datagram 处先满)后 supervisor **阻塞在 `sendmsg`** 直到会话结束——不再 accept、不再回 Busy、不再 reap。 + +**缺陷 4。** 非 `--serve` 的单会话 TCP 模式为 `ListenerSource` 留着监听者,数据连接绑定后没人关它:内核继续 +完成三次握手进 backlog,第二个 client / 重试要等满 `kSpawnConnectTimeoutMs`(20 s)而不是像改前那样立即 +`ECONNREFUSED`。 + +**修法(同一个钩子)。** `RunSession` 多收一个 `sourceFd`——`--serve` 下是子进程那端 `pair[1]`,单会话下是 +监听者——`session.Accept()` 一返回就 `close` 并清掉 `DataConnectionSource`。于是 (1) supervisor 再转发时 +`sendmsg` 得到 `ECONNREFUSED`/`ENOTCONN`(`FdPassing::SendFd` 把「对端已走」当答案而不是错误日志),已有的 +`Refuse{Authentication} data connection could not be handed to the live session` 按名字打出;`SendFd` 加 +`dontWait`(`MSG_DONTWAIT`),队列因别的原因满也拒绝而不是卡住;(4) 单会话的第二个连接立即 `ECONNREFUSED`。 + +**red-once**(server 还原到 `da313010`,只重建 `.so`): + +``` +RED smoke data_bind_after_bound: TimeoutError: timed out(第一条多余 DataBind 的 receive 就超时——没人回) +RED probe f1probe.py(qlen+2 条 DataBind 只发不收): extras opened=288 of 514 后 connect/send 超时; + answered by name=0 silent=288;此后新开控制连接: CONNECT FAILED TimeoutError(supervisor 卡在 sendmsg); + supervisor alive=True +RED smoke single_session_listener: 绑定后的第二个 connect 成功(监听者还开着) +GREEN smoke data_bind_after_bound: refused=12,每条 Refuse{Authentication} 'data connection could not be + handed to the live session';随后新开控制连接 → Refuse{Busy} +GREEN probe: 514/514 具名拒绝、silent=0、新控制连接 → Busy、supervisor alive +GREEN smoke single_session_listener: second_connect=ConnectionRefusedError(111) +``` + +smoke 常驻控制两条:`data_bind_after_bound`(会话 `ready` 后 12 条多余 DataBind 各得具名拒绝、再开控制连接仍 +Busy)与 `single_session_listener`(无 `--serve` 的 supervisor,`ready` 后 connect 必须 `ECONNREFUSED`)。 +`held_session` / `pair` 共用 `connect_control`(启动期 `ECONNREFUSED` 重试 5 s)。 + +### 6.2 修复 2(`75f24db0`):`sha256(protocol.fbs)` 钉住 revision + +revision 是手改整数,改了 `protocol.fbs` 不 bump 就回到「新旧对端指纹相同、字段静默缺失」。 +`scripts/ci/protocol_revision_pin.py` 持有表 `{revision → sha256(protocol.fbs)}` +(`protocol_revision_pins.json`,行尾折成 LF 再哈希),当前 revision 没有行、或行与文件摘要不符都红;`--write` +在 bump 后记新行,且拒绝把已有 revision 改钉到另一个摘要(那正是「改 schema 没 bump」的样子)。注册为 +`ProtocolSchema.RevisionPinsDigest`(label unit,`--self-test` 先证明两种扰动都红才说绿),并作为 CI +`flatc-check` 作业的一步——正是每次 schema 改动都要重新生成头文件的那个作业。 + +``` +RED protocol.fbs 末尾加一行注释、revision 仍为 1: + ::error::protocol.fbs has sha256 a7b5c7fb888d2926... but MOBILEGL_PROTOCOL_CONTROL_REVISION is still 1, + which is pinned to 7fc859f7fa756818... rc=1 +GREEN 还原: protocol revision pin: revision 1 holds (sha256 7fc859f7fa756818..., 1 revision(s) pinned, + self-test red on both perturbations) rc=0 +``` + +### 6.3 nit 6 / 8 / 9 / 10(`2b2a0ae0`) + +- **nit 6** `AuthToken.h`:改前循环长度 = 本端令牌长度,响应时间正比于**秘密**的长度而头注释写得像什么都不泄。 + 现在循环固定 `kAuthTokenCompareBytes = 64`(更长的令牌延到其长度,只泄露「长于 64」),两端越界读零、长度差 + 仍并入累加器、不延到对端长度(不让对端选我们的工作量)。`SocketTransportTest.TheTokenComparisonAnswersLength + AndPrefixTheSameWay` 加 70 字节令牌最后一字节判定的用例。 +- **nit 8** `ServerSession::Accept`:请求超过所授条款 → 一条 `MGLOG_W`:`Hello asked for windows the server + does not grant (cmd=68719476736 ...); the ask is ignored and the session is server-sized (cmd=8388608 + stage=33554432 event=262144 maxReply=2097136) - PH-8`。单测 §3.1 那条现在要求这一行(改前红:行不存在)。 +- **nit 9** `ServerSession::Accept`:`MintNonce` 失败先在控制连接上回 `Refuse{LinkTerms} stream data plane + unavailable: the server could not mint a data nonce`,再返回原错误(仍是 67、仍计入 `sessionsFaulted`—— + CSPRNG 给不出字节是 server 的故障不是对端的条款)。没有驱动:不注入故障造不出 `getrandom` + `/dev/urandom` + 同时失败,记录于此。 +- **nit 10** `RouteWhileBusy`:有活会话时首帧 magic 错或超 1 MiB(`ReceiveOneFrame` 回 `PROTOCOL_MISMATCH`) + 改回 `Refuse{MalformedHello} first frame is not a control frame`(`Accept` 对不可验证 Hello 用的同一个词), + 关闭前先把对端已发的字节读掉免得 RST 冲掉拒绝帧;格式正确但不是 DataBind 的帧仍是 Busy。smoke 常驻控制 + `garbage_first_frame_while_busy`(magic `XXXX` → code 8;改前红:code 7)。普查仍 3 个 refusal 词(新拒绝 + 走枚举值,源码里没有新字面量)。 + +### 6.4 记录债(本轮补记;CONTRACT-P7 §12 同步) + +- **unix 端点仍按到达顺序配对**(`AcceptPair` 的 250 ms / 2000 ms 窗口)。`SocketTransport.h` 把它写成 + 「同用户 rendezvous、别人够不着」——但抽象命名空间套接字**没有 mode bits**,「只有本 app 能连」这条性质靠的是 + Android 的 SELinux 域隔离,不是传输层自己。本地对端债,不在 Ph 的远端威胁模型里,未做。 +- **两处 wave 2-F 之前就存在的预认证暴露**(审查核实、归 PH-7 (5)):(a) supervisor 对每条 TCP 连接**先 fork + 再读 Hello**,未认证对端每连接可让 server 付出一次 fork + 最长 10 s 的 Hello 等待;(b) `ValidatePeerHandshake` + 在 `AuthenticatePeerToken` **之前**跑,`Refuse{ProtocolVersion|WireFingerprint|BuildFingerprint}` 把本端 + wire 指纹与 build stamp 送给了未认证的对端。 +- 令牌最小长度只在 TCP 监听处检查(§1.1)。 + +--- + +## 7. 门(F 修复轮的头 `2b2a0ae0`,包树 SHA;F-d 只改文档,二进制同此头) + +读数 `~/w7/logs/p7-ph-f-fgate.log`、车道日志 `~/w7/logs/p7-ph-f-ffinal-*.log`。 + +| 门 | 片 1–3 头 `a746417a` | F 修复轮头 | +|---|---|---| +| G1 pull `.text` / 符号增删 | `0xa52203` / 0/0 | `0xa52203` / 0/0(`build-linux` 无工作;`compile_commands.json` 里 `MG_Remote` 条目 0——簇 F 文件不在 pull 构建,`mg_protocol_base.h` 只改注释) | +| `fatal_census.py` | 79 abort、44 家族词、3 refusal 词、0 未标记 | 同左(新拒绝走枚举值,无新字面量) | +| `link_ratchet.py --assert-monotone` | 173 | 173 | +| `spawn_lane_parity.py` | 0 错 | rc 0(split 193 / spawn 190 / tcp 190) | +| `protocol_revision_pin.py --self-test` | — | revision 1 holds,两种扰动均红 | +| G14 名集合 | — | 对 `~/w7/p7-before` 基线 removed=1(`CapsMirrorTest.APlaceholderMirrorConsumesNothing`,在本包基线 `da313010` 上已不存在,是更早的 P7 波次退的)、added=2819;本轮只增 `ProtocolSchema.RevisionPinsDigest` | +| unit | 2433 | 2434(+1:`ProtocolSchema.RevisionPinsDigest`;`SessionHandshakeTest` / `SocketTransportTest` 的新断言在既有用例内) | +| integration-tcp | 222 | 222(`TcpLane.SupervisorProtocolControls` Passed,evidence 17 项含 `data_bind_after_bound` / `garbage_first_frame_while_busy` / `single_session_listener`) | +| integration-spawn | 219 | 219 | +| integration-magma-tcp | 74 | 74 |