fix: 增强日志清理的安全性和可靠性

必须修复的问题:
1. PID重用防护 - 添加进程启动时间检查,对比文件修改时间避免误删活动进程的日志
   - Unix: 通过 /proc/<pid>/stat 读取进程启动时间
   - Windows: 使用 GetProcessTimes API 获取创建时间
   - 7天策略: 无法获取进程启动时间时,超过7天的日志视为孤儿

2. 符号链接攻击防护 - 新增安全检查避免删除恶意符号链接
   - 使用 os.Lstat 检测符号链接
   - 使用 filepath.EvalSymlinks 解析真实路径
   - 确保所有文件在 TempDir 内(防止路径遍历)

强烈建议的改进:
3. 异步启动清理 - 通过 goroutine 运行清理避免阻塞主流程启动

4. NotExist错误语义修正 - 文件已被其他进程删除时计入 Kept 而非 Deleted
   - 更准确反映实际清理行为
   - 避免并发清理时的统计误导

5. Windows兼容性验证 - 完善Windows平台的进程时间获取

测试覆盖:
- 更新所有测试以适配新的安全检查逻辑
- 添加 stubProcessStartTime 支持PID重用测试
- 修复 setTempDirEnv 解析符号链接避免安全检查失败
- 所有测试通过(codex-wrapper: ok 6.183s)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
swe-agent[bot]
2025-12-08 10:53:52 +08:00
parent 9452b77307
commit da257b860b
6 changed files with 317 additions and 46 deletions

View File

@@ -201,8 +201,7 @@ func TestRunTerminateProcessNil(t *testing.T) {
}
func TestRunCleanupOldLogsRemovesOrphans(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
orphan1 := createTempLog(t, tempDir, "codex-wrapper-111.log")
orphan2 := createTempLog(t, tempDir, "codex-wrapper-222-suffix.log")
@@ -215,6 +214,15 @@ func TestRunCleanupOldLogsRemovesOrphans(t *testing.T) {
return runningPIDs[pid]
})
// Stub process start time to be in the past so files won't be considered as PID reused
stubProcessStartTime(t, func(pid int) time.Time {
if runningPIDs[pid] {
// Return a time before file creation
return time.Now().Add(-1 * time.Hour)
}
return time.Time{}
})
stats, err := cleanupOldLogs()
if err != nil {
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
@@ -243,8 +251,7 @@ func TestRunCleanupOldLogsRemovesOrphans(t *testing.T) {
}
func TestRunCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
invalid := []string{
"codex-wrapper-.log",
@@ -263,6 +270,10 @@ func TestRunCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
return false
})
stubProcessStartTime(t, func(pid int) time.Time {
return time.Time{} // Return zero time for processes not running
})
removeErr := errors.New("remove failure")
callCount := 0
stubRemoveLogFile(t, func(path string) error {
@@ -302,6 +313,9 @@ func TestRunCleanupOldLogsHandlesGlobFailures(t *testing.T) {
t.Fatalf("process check should not run when glob fails")
return false
})
stubProcessStartTime(t, func(int) time.Time {
return time.Time{}
})
globErr := errors.New("glob failure")
stubGlobLogFiles(t, func(pattern string) ([]string, error) {
@@ -321,13 +335,15 @@ func TestRunCleanupOldLogsHandlesGlobFailures(t *testing.T) {
}
func TestRunCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
setTempDirEnv(t, t.TempDir())
stubProcessRunning(t, func(int) bool {
t.Fatalf("process check should not run for empty directory")
return false
})
stubProcessStartTime(t, func(int) time.Time {
return time.Time{}
})
stats, err := cleanupOldLogs()
if err != nil {
@@ -339,8 +355,7 @@ func TestRunCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
}
func TestRunCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
paths := []string{
createTempLog(t, tempDir, "codex-wrapper-6100.log"),
@@ -348,6 +363,7 @@ func TestRunCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
}
stubProcessRunning(t, func(int) bool { return false })
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
var attempts int
stubRemoveLogFile(t, func(path string) error {
@@ -379,13 +395,13 @@ func TestRunCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
}
func TestRunCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
protected := createTempLog(t, tempDir, "codex-wrapper-6200.log")
deletable := createTempLog(t, tempDir, "codex-wrapper-6201.log")
stubProcessRunning(t, func(int) bool { return false })
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
stubRemoveLogFile(t, func(path string) error {
if path == protected {
@@ -416,19 +432,20 @@ func TestRunCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
}
func TestRunCleanupOldLogsPerformanceBound(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
const fileCount = 400
fakePaths := make([]string, fileCount)
for i := 0; i < fileCount; i++ {
fakePaths[i] = filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d.log", 10000+i))
name := fmt.Sprintf("codex-wrapper-%d.log", 10000+i)
fakePaths[i] = createTempLog(t, tempDir, name)
}
stubGlobLogFiles(t, func(pattern string) ([]string, error) {
return fakePaths, nil
})
stubProcessRunning(t, func(int) bool { return false })
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
var removed int
stubRemoveLogFile(t, func(path string) error {
@@ -468,8 +485,7 @@ func TestRunLoggerCoverageSuite(t *testing.T) {
}
func TestRunCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
tempDir := t.TempDir()
setTempDirEnv(t, tempDir)
tempDir := setTempDirEnv(t, t.TempDir())
currentPID := os.Getpid()
currentLog := createTempLog(t, tempDir, fmt.Sprintf("codex-wrapper-%d.log", currentPID))
@@ -480,6 +496,12 @@ func TestRunCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
}
return true
})
stubProcessStartTime(t, func(pid int) time.Time {
if pid == currentPID {
return time.Now().Add(-1 * time.Hour)
}
return time.Time{}
})
stats, err := cleanupOldLogs()
if err != nil {
@@ -580,11 +602,16 @@ func createTempLog(t *testing.T, dir, name string) string {
return path
}
func setTempDirEnv(t *testing.T, dir string) {
func setTempDirEnv(t *testing.T, dir string) string {
t.Helper()
t.Setenv("TMPDIR", dir)
t.Setenv("TEMP", dir)
t.Setenv("TMP", dir)
resolved := dir
if eval, err := filepath.EvalSymlinks(dir); err == nil {
resolved = eval
}
t.Setenv("TMPDIR", resolved)
t.Setenv("TEMP", resolved)
t.Setenv("TMP", resolved)
return resolved
}
func stubProcessRunning(t *testing.T, fn func(int) bool) {
@@ -596,6 +623,15 @@ func stubProcessRunning(t *testing.T, fn func(int) bool) {
})
}
func stubProcessStartTime(t *testing.T, fn func(int) time.Time) {
t.Helper()
original := processStartTimeFn
processStartTimeFn = fn
t.Cleanup(func() {
processStartTimeFn = original
})
}
func stubRemoveLogFile(t *testing.T, fn func(string) error) {
t.Helper()
original := removeLogFileFn