ソースを参照

fix(v7): writeAtomicBatch 总闸显式拒绝绝对路径(修 ubuntu CI)

CI ubuntu 挂在 atomic 越界测试:POSIX 的 path.join(repo, 绝对路径) 会把绝对
路径吸收进 repo 内部(/repo + /tmp/x → /repo/tmp/x),包含性检查判定"仍在
仓内"→ 不拒绝;Windows 对盘符处理不同才碰巧落到仓外通过。总闸因此平台相关
地漏判绝对路径。

修法:mkdir 前先 path.isAbsolute(f.path) 显式拒,再做包含检查——绝对路径向量
与相对 .. 逃逸向量分别由两道机制兜住,跨平台一致。测试补相对 .. 逃逸断言,
锁住两道机制不再各自回归。

验证:Windows 全量 695 绿、WSL/POSIX atomic 6/6 绿、独立变体探针全过。
(WSL /mnt/d 上另有 2 个 persist git-回滚测试失败,经 stash 比对确认为 drvfs
挂载既有噪声,与本改动和 P0 无关,真 ubuntu CI 上 persist 正常。)
lingfengQAQ 2 ヶ月 前
親
コミット
397b133071
2 ファイル変更、12 行追加、1 行削除
  1. 6 1
      v7/src/storage/atomic.js
  2. 6 0
      v7/test/storage/atomic.test.js

+ 6 - 1
v7/src/storage/atomic.js

@@ -18,11 +18,16 @@ export async function writeAtomicBatch(repoPath, files) {
     for (const f of files) {
       if (seen.has(f.path)) throw new Error(`批量写入包含重复路径:${f.path}`)
       seen.add(f.path)
-      const full = path.join(repoPath, f.path)
       // P0-F4 总闸:不信任任何调用方,词法级 repo 边界断言(目标可能尚不存在,
       // 故不做 realpath;与 staging/contract-invalidation.js 的
       // workspaceRemovalIsContained 判定口径不同构,两处注释互指)。
       // 必须在 mkdir 之前判定:越界时零建目录。
+      // 绝对路径先拒:POSIX 的 path.join(repo, 绝对路径) 会把它吸收进 repo 内部
+      //(Windows 对盘符处理又不同),仅靠包含检查会平台相关地漏判,故显式拦。
+      if (path.isAbsolute(f.path)) {
+        throw new Error(`批量写入路径越界:${f.path}`)
+      }
+      const full = path.join(repoPath, f.path)
       const resolved = path.resolve(full)
       if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
         throw new Error(`批量写入路径越界:${f.path}`)

+ 6 - 0
v7/test/storage/atomic.test.js

@@ -31,12 +31,18 @@ test('writeAtomicBatch:../ 越界路径整批拒绝且仓外零残留', async
 test('writeAtomicBatch:绝对路径与深嵌套越界同样拒绝', async () => {
   const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-atomic-'))
   try {
+    // 绝对路径:POSIX 的 path.join(root, 绝对路径) 会吸收进 repo 内部,仅靠包含检查会漏判,
+    // 靠 isAbsolute 显式拒(跨平台一致)。
     await assert.rejects(() =>
       writeAtomicBatch(root, [{ path: path.join(root, '..', 'deep', 'x.md'), content: 'x' }])
     )
     await assert.rejects(() =>
       writeAtomicBatch(root, [{ path: path.resolve(path.join(root, 'a', '..', '..', 'x.md')), content: 'x' }])
     )
+    // 相对 .. 逃逸:非绝对,靠边界包含检查拒。
+    await assert.rejects(() =>
+      writeAtomicBatch(root, [{ path: path.join('..', '逃逸.md'), content: 'x' }])
+    )
     assert.deepEqual(await fs.readdir(root), [], '批内应零落盘')
   } finally {
     await fs.rm(root, { recursive: true, force: true })