test(drive): make pinned-root TOCTOU reproductions runnable on Windows

Windows keeps the pinned directory locked while a handle inside it is open
(os.Root plus the pull temp file or the upload source), so renaming that
directory fails with a sharing violation. Every "pinned root/ancestor was
swapped" reproduction in the drive mirror tests relied on such a rename, so 13
tests failed on windows-latest. That, not a coverage shortfall, is why
Coverage (Windows) exited 1 before the gate ever ran.

Each reproduction now falls back to injecting the equivalent identity change
when the rename is refused. pinnedPullRoot.verify() and verifyParent() read
current identity only through pullPathStat / pullRootLstat, so pointing those
seams at another directory hits the same fail-closed branches. Unix still
performs the real move and loses no strength.

Assertions that need an actual replacement tree now branch on the helper's
return value. forcePinnedFallbackForTest makes the fallback path itself
regressible on any platform, and a dedicated test covers it.

Verified locally with the fallback forced on: all 13 tests pass and changed
code coverage stays at 100%.
This commit is contained in:
chichuan committed 2026-08-13 21:35:48 +08:00
1 parent 504db23823
commit 97b6022017
4 files changed
+159 -31

No files matched your search

@@ -0,0 +1,77 @@
package helpers
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
// Windows 会用已打开的目录句柄锁住目录,把「固定后目录被移走/换成软链」这类
// TOCTOU 在该平台变成物理不可达:os.Rename 直接 ERROR_SHARING_VIOLATION。
// 但被测的 fail-closed 分支必须在所有平台都覆盖,所以这里提供与真实替换等价的
// 身份注入:pinnedPullRoot.verify() 与 verifyParent() 都只通过 pullPathStat /
// pullRootLstat 读取当前身份,让这两个 seam 指向另一个目录即可命中同一分支。
// forcePinnedFallbackForTest 打开后,「移走固定目录」的复现手法一律走身份注入降级。
// 默认 false:Unix 上用真实移动,验证更强。只有 Windows 才会真的走降级分支,所以
// 下面的注入回归测试会打开它,让那条路径在任何平台都能被验证。
var forcePinnedFallbackForTest = false
// swapPinnedRootIdentity 让 absDir 的根身份读数指向另一个目录,使随后的
// pinnedPullRoot.verify() 判定「本地根目录在同步期间被替换」。
func swapPinnedRootIdentity(t *testing.T, absDir string) {
t.Helper()
decoy, err := os.Stat(t.TempDir())
if err != nil {
t.Fatal(err)
}
want := filepath.Clean(absDir)
testseam.Swap(t, &pullPathStat, func(path string) (os.FileInfo, error) {
if filepath.Clean(path) == want {
return decoy, nil
}
return os.Stat(path)
})
}
// 降级注入只有 Windows 会真的走到(Unix 的 rename 成功)。这里强制打开开关,
// 确保注入手段本身在任何平台都可回归:不移动目录,但固定根的校验必须随之失败,
// 且原有目录树不得被动过。
func TestCrossPlatformCoveragePinnedRootFallbackInjectionFailsVerification(t *testing.T) {
testseam.Swap(t, &forcePinnedFallbackForTest, true)
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "a.txt"), "pinned-original")
root := newPinnedPullRootForCoverage(t, dir)
if err := root.verify(); err != nil {
t.Fatalf("baseline verify: %v", err)
}
if moved := replacePinnedMirrorRoot(t, dir, filepath.Join(t.TempDir(), "moved")); moved {
t.Fatal("降级路径不应真的移动目录")
}
if err := root.verify(); err == nil || !strings.Contains(err.Error(), "本地根目录在同步期间被替换") {
t.Fatalf("身份注入后校验必须失败,got %v", err)
}
if b, err := os.ReadFile(filepath.Join(dir, "a.txt")); err != nil || string(b) != "pinned-original" {
t.Fatalf("降级必须保持原目录树不变: %q err=%v", string(b), err)
}
}
// swapPinnedParentIdentity 让固定根下名为 rel 的父目录身份读数指向另一个目录,
// 使随后的 verifyParent() 判定「本地目标目录在下载期间被替换」。
func swapPinnedParentIdentity(t *testing.T, rel string) {
t.Helper()
decoy, err := os.Lstat(t.TempDir())
if err != nil {
t.Fatal(err)
}
testseam.Swap(t, &pullRootLstat, func(parent *os.Root, name string) (os.FileInfo, error) {
if filepath.ToSlash(filepath.Clean(name)) == rel {
return decoy, nil
}
return parent.Lstat(name)
})
}
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -350,9 +351,13 @@ func TestCrossPlatformCoverageDrivePullPlanCallers(t *testing.T) {
if name == "a.txt" && !moved {
moved = true
if renameErr := os.Rename(dir, dir+"-pinned"); renameErr != nil {
t.Fatal(renameErr)
}
if mkdirErr := os.Mkdir(dir, 0o755); mkdirErr != nil {
// Windows 在固定目录上持有句柄时会锁住它,移走于该平台物理
// 不可达;注入等价的根身份变化,命中同一条二次校验分支。
if runtime.GOOS != "windows" {
t.Fatal(renameErr)
}
swapPinnedRootIdentity(t, dir)
} else if mkdirErr := os.Mkdir(dir, 0o755); mkdirErr != nil {
t.Fatal(mkdirErr)
}
}
+37 -19
View File
@@ -8,6 +8,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -30,8 +31,19 @@ func replacePullAncestorWithOutsideLink(t *testing.T, root, outside string) stri
t.Helper()
sub := filepath.Join(root, "sub")
parked := filepath.Join(root, "sub-pinned")
if forcePinnedFallbackForTest {
swapPinnedParentIdentity(t, "sub")
return sub
}
if err := os.Rename(sub, parked); err != nil {
t.Fatal(err)
// Windows 在目录内有已打开的句柄(如 pull 的临时文件)时会锁住该目录,
// 移走再换链于该平台物理不可达。退化为等价的父目录身份注入,命中
// verifyParent 同一条 fail-closed 分支;原目录未移动,故返回 sub。
if runtime.GOOS != "windows" {
t.Fatal(err)
}
swapPinnedParentIdentity(t, "sub")
return sub
}
if err := os.Symlink(outside, sub); err != nil {
t.Skipf("平台不支持创建符号链接: %v", err)
@@ -39,6 +51,27 @@ func replacePullAncestorWithOutsideLink(t *testing.T, root, outside string) stri
return parked
}
// replacePullCommandRootWithOutsideLink 把命令根整体移走,并在原名处放置指向根外的
// 符号链接,复现「固定后命令根被换掉」的场景。Windows 在根目录持有已打开句柄时会
// 锁住该目录,移走于该平台物理不可达,此时退化为等价的根身份注入。
func replacePullCommandRootWithOutsideLink(t *testing.T, root, parked, outside string) {
t.Helper()
if forcePinnedFallbackForTest {
swapPinnedRootIdentity(t, root)
return
}
if err := os.Rename(root, parked); err != nil {
if runtime.GOOS != "windows" {
t.Fatal(err)
}
swapPinnedRootIdentity(t, root)
return
}
if err := os.Symlink(outside, root); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
}
func runPullTOCTOUCase(t *testing.T, command string, root string) error {
t.Helper()
rf := &remoteFile{RelPath: "sub/a.txt", FileID: "F"}
@@ -587,12 +620,7 @@ func TestCrossPlatformCoverageDrivePullCommandPinsRootAcrossAllFiles(t *testing.
return err
}
if calls == 1 {
if err := os.Rename(root, parked); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, root); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
replacePullCommandRootWithOutsideLink(t, root, parked, outside)
}
return nil
})
@@ -622,12 +650,7 @@ func TestCrossPlatformCoverageDrivePullPlansExistingRootBeforeRemoteRead(t *test
if tool != "list_files" {
t.Fatalf("root replacement must stop before %s", tool)
}
if err := os.Rename(root, parked); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, root); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
replacePullCommandRootWithOutsideLink(t, root, parked, outside)
return `{"result":{"items":[{"name":"a.txt","type":"file","fileId":"A"}]}}`, nil
}}
getCalls := 0
@@ -727,12 +750,7 @@ func TestCrossPlatformCoverageDriveSyncCommandPinsRootAcrossAllFiles(t *testing.
return err
}
if calls == 1 {
if err := os.Rename(root, parked); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, root); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
replacePullCommandRootWithOutsideLink(t, root, parked, outside)
}
return nil
})
+37 -9
View File
@@ -5,21 +5,37 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
func replacePinnedMirrorRoot(t *testing.T, root, moved string) {
// replacePinnedMirrorRoot 返回 true 表示原目录被真实移走、原名处已换成另一个目录;
// 返回 false 表示走了 Windows 身份注入降级,原目录仍留在原处。依赖「替代目录内容」
// 的断言必须按返回值分流。
func replacePinnedMirrorRoot(t *testing.T, root, moved string) bool {
t.Helper()
if forcePinnedFallbackForTest {
swapPinnedRootIdentity(t, root)
return false
}
if err := os.Rename(root, moved); err != nil {
t.Fatalf("move pinned root: %v", err)
// Windows 在目录内有已打开的句柄(如上传读取中的源文件)时会锁住该目录,
// 移走固定根于该平台物理不可达。退化为等价的根身份注入,命中同一条
// fail-closed 分支。
if runtime.GOOS != "windows" {
t.Fatalf("move pinned root: %v", err)
}
swapPinnedRootIdentity(t, root)
return false
}
if err := os.Mkdir(root, 0o755); err != nil {
t.Fatalf("create replacement root: %v", err)
}
mustWrite(t, filepath.Join(root, "a.txt"), "outside-replacement")
return true
}
// 本地扫描完成后、任何 create_folder/get_upload_info/commit_upload 前若命令行根已
@@ -36,10 +52,11 @@ func TestCrossPlatformCoverageDrivePushSyncRejectRootReplacementBeforeRemoteWrit
mustWrite(t, filepath.Join(root, "a.txt"), "pinned-original")
replaced := false
realMove := false
caller := &driveScriptCaller{reply: func(tool string, _ map[string]any, _ int) (string, error) {
if tool == "list_files" {
if !replaced {
replacePinnedMirrorRoot(t, root, moved)
realMove = replacePinnedMirrorRoot(t, root, moved)
replaced = true
}
return `{"result":{"items":[],"nextToken":""}}`, nil
@@ -68,7 +85,12 @@ func TestCrossPlatformCoverageDrivePushSyncRejectRootReplacementBeforeRemoteWrit
if putCalls != 0 {
t.Fatalf("OSS PUT must not run after root replacement, got %d", putCalls)
}
if b, err := os.ReadFile(filepath.Join(root, "a.txt")); err != nil || string(b) != "outside-replacement" {
want := "outside-replacement"
if !realMove {
// 身份注入降级:原目录仍在原处,内容同样不得被镜像流程改写。
want = "pinned-original"
}
if b, err := os.ReadFile(filepath.Join(root, "a.txt")); err != nil || string(b) != want {
t.Fatalf("replacement tree changed: %q err=%v", string(b), err)
}
})
@@ -100,9 +122,10 @@ func TestCrossPlatformCoverageDrivePushSyncUploadReadsPinnedFileHandle(t *testin
var uploaded string
putCalls := 0
realMove := false
testseam.Swap(t, &pushPutOpenedFile, func(_ context.Context, _ string, _ map[string]string, file *os.File, _ int64) error {
putCalls++
replacePinnedMirrorRoot(t, root, moved)
realMove = replacePinnedMirrorRoot(t, root, moved)
body, err := io.ReadAll(file)
if err != nil {
return err
@@ -125,10 +148,15 @@ func TestCrossPlatformCoverageDrivePushSyncUploadReadsPinnedFileHandle(t *testin
if commits := caller.callsFor("commit_upload"); len(commits) != 0 {
t.Fatalf("root replacement must prevent commit_upload: %v", commits)
}
if b, readErr := os.ReadFile(filepath.Join(root, "a.txt")); readErr != nil || string(b) != "outside-replacement" {
t.Fatalf("replacement file was read or changed: %q err=%v", string(b), readErr)
}
if b, readErr := os.ReadFile(filepath.Join(moved, "a.txt")); readErr != nil || string(b) != "pinned-original" {
if realMove {
if b, readErr := os.ReadFile(filepath.Join(root, "a.txt")); readErr != nil || string(b) != "outside-replacement" {
t.Fatalf("replacement file was read or changed: %q err=%v", string(b), readErr)
}
if b, readErr := os.ReadFile(filepath.Join(moved, "a.txt")); readErr != nil || string(b) != "pinned-original" {
t.Fatalf("pinned original changed: %q err=%v", string(b), readErr)
}
} else if b, readErr := os.ReadFile(filepath.Join(root, "a.txt")); readErr != nil || string(b) != "pinned-original" {
// 身份注入降级:原目录仍在原处,内容同样不得被上传路径改写。
t.Fatalf("pinned original changed: %q err=%v", string(b), readErr)
}
})