fix(store): 修正邻接查询、GC 竞态、备份权限与设置解析,新增 OpenData 纯数据打开

邻接查询 tie-break 与 pinned 排序键与列表序对齐,补表驱动测试(round2 P1-1); GC DELETE 带条件复查,防 SELECT 与 DELETE 间恢复/引用竞态(P2-6); 备份经 umask 收紧创建即 0600,消除 chmod 前暴露窗口(P2-7); page_size 改 strconv.Atoi 全文解析,拒部分解析脏值(P2-13); OpenData 不迁移、不做版本守卫(D6/P1-5)。
This commit is contained in:
2026-09-08 17:32:47 +08:00
parent a885b52f18
commit 132c4a1ba2
5 changed files with 162 additions and 26 deletions
+25 -4
View File
@@ -3,6 +3,7 @@ package store
import (
"fmt"
"os"
"syscall"
"time"
)
@@ -86,15 +87,29 @@ func (s *Store) GC(now time.Time, dryRun bool) (*GCReport, error) {
return rep, nil
}
// 条件复查删除(防 TOCTOU:SELECT 与 DELETE 之间管理员恢复笔记/图片被
// 重新引用时,不删活数据;评审 round2 P2-6)
for _, en := range rep.ExpiredNotes {
if err := s.DeleteNoteForever(en.ID); err != nil {
res, err := s.db.Exec(
`DELETE FROM notes WHERE id=? AND deleted_at IS NOT NULL AND deleted_at < ?`,
en.ID, noteCutoff)
if err != nil {
return nil, fmt.Errorf("物理删除笔记 %d: %w", en.ID, err)
}
if rows, _ := res.RowsAffected(); rows == 0 {
continue // 期间已被恢复,跳过
}
}
for _, img := range rep.OrphanImages {
if err := s.DeleteImage(img.ID); err != nil {
res, err := s.db.Exec(
`DELETE FROM images WHERE id=? AND NOT EXISTS (SELECT 1 FROM image_refs WHERE image_id=?)`,
img.ID, img.ID)
if err != nil {
return nil, fmt.Errorf("删除孤儿图片 %d: %w", img.ID, err)
}
if rows, _ := res.RowsAffected(); rows == 0 {
continue // 期间已被引用,跳过
}
}
n, err := s.DeleteExpiredSessions(now.Unix())
if err != nil {
@@ -109,11 +124,17 @@ func (s *Store) Backup(destPath string) error {
if _, err := os.Stat(destPath); err == nil {
return fmt.Errorf("目标文件已存在: %s", destPath)
}
// 备份含全部私密内容:临时收紧 umask,确保 VACUUM INTO 创建即 0600
// (否则以默认 umask(常 0644)创建、事后 chmod 存在暴露窗口,评审 round2 P2-7)。
// backup 为 CLI 单线程进程,进程级 umask 无并发副作用。
old := syscall.Umask(0o077)
// VACUUM INTO 不接受参数绑定,路径经单引号转义(无参数化通道时的最小注入面)。
if _, err := s.db.Exec("VACUUM INTO " + escapeSQLString(destPath)); err != nil {
_, err := s.db.Exec("VACUUM INTO " + escapeSQLString(destPath))
syscall.Umask(old)
if err != nil {
return err
}
// 备份含全部私钥内容:强制 0600(§10.3)
// 双保险:显式收紧(幂等)
if err := os.Chmod(destPath, 0o600); err != nil {
return err
}