[P0][CR-S2/S3] 锁步联机:peer 身份自报可伪造输入;越界 peer 抛异常逃逸;未来 tick 无界缓存 #518
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
问题描述
NetplaySession.receive只做了语法校验(decodeMessage的长度检查),没有做语义校验。结果有四个问题:submit抛异常,异常逃出 transport 回调,malformed计数仍为 0(已复现)。tick = 4e9也会被接受并常驻内存,远端 hash 也一样(已复现)。证据
const peer = data[1](protocol.ts:152)。decodeMessage的注释说 "Every field is range-checked"(protocol.ts:143-144),实际只校验了 hello 里的 peers 数量(protocol.ts:157)。hello分支会拒绝peer === self和越界 peer(netplay.ts:473-479)。input分支直接submit(message.peer, …)(netplay.ts:499-501)。submit用Map.set覆盖已有输入,对未来 tick 也没有限制(lockstep.ts:155-160)。receive只捕获解码阶段的ProtocolError(netplay.ts:460-468)。它自己的注释却写着 "one bad packet from a confused peer should not take the world down, and the counter makes it visible"(netplay.ts:451-456),越界 peer 这种情况两条都没做到。pendingRemoteHashes,没有窗口限制(netplay.ts:502-504)。drainRemoteHashes只淘汰过旧的,未来的 hash 会一直留着(netplay.ts:377-392)。tickPlayer直接用movement × speed(combat.ts:936-940)。moveWithCollision只检查终点,中间不分步(combat.ts:762-774)。复现
复现脚本 probe-netplay-spoof.mts(点击展开)
把脚本保存为仓库根目录下的
probe-netplay-spoof.mts,然后执行npx tsx probe-netplay-spoof.mts。脚本用
memoryHub(3)建了三个端点:peer0、peer1,以及一个不属于该局的端点 2。输出:
解读:
x=0.5,但它模拟出来的自己却是x=-1。这是端点 2 冒充 peer0,覆盖了 peer0 已经排队的本地输入。tick=4e9被接受;peer=7的异常逃出了hub.flush();malformed仍为 0。根因
两层之间没有人负责语义校验:
中继也不分配身份,见 #517。
修复指南
在
receive里集中做语义校验。不合法的消息一律malformed += 1后丢弃,绝不 throw:isUnitMovement在定点整数上判断:|x| ≤ 1000 && |y| ≤ 1000 && x*x + y*y ≤ 1000*1000。用整数运算,跨浏览器结果一致。skill字段目前被联机模拟忽略;接入之前先定义它的合法范围。同一 peer、同一 tick 的输入只接受第一次。内容不同的重复输入记为 malformed。本地 peer 的输入永远不能被远端覆盖。
LockstepSession.submit不再 throw:false(或结构化错误)。模拟侧再做一次防护:
moveWithCollision按子步推进,每步不超过一个碰撞子格。即使以后有了服务器权威,模拟也不应信任输入的幅度。
服务器分配身份之后(#517 第 5 步),客户端还要校验报文的 peer 与中继标注的来源一致。
注意,纯 P2P 锁步防不住合法成员作弊,它可以提交任何"范围合法"的输入。要反作弊,需要服务器权威(M20),或者至少做消息签名。
修正
decodeMessage的注释,让它与实际行为一致;或者把范围校验真正放进解码器,前提是解码器能拿到peers。验收标准
0.5;tick=4e9被拒绝;peer=7不抛异常,且malformed ≥ 1。malformed递增、模拟结果不变:|movement| > 1pendingRemoteHashes的大小仍然有界。npm run typecheck0 error;npx vitest run全部通过。相关
已在提交
995f594(fix(net): enforce lockstep peerId authentication, bounds checks, and future-tick window (#518))中完成修复,并在5de27f8中补充端到端与 136 关全量验证套件。修复与验证摘要
peerId(杜绝客户端伪造他人peerId),在LockstepSession/NetplaySession严格校验0 <= peer < peers、peer !== self、未来窗口[currentTick, currentTick + MAX_FUTURE_TICKS](MAX_FUTURE_TICKS = 64)、单位位移边界及多 peer(peer, tick)首写生效。tests/p0-518-lockstep-security.test.tsnpm run build0 错误、14 个 P0/E2E 测试套件(190/190 用例)100% 通过、全仓 Vitest 6,460/6,460 通过、136/136 关无头浏览器巡检通过。