Skip to content

VACUUM INTO fails with SQLITE_IOERR_LOCK/EBADF on any already-WAL database: vacuum dest inherits SQLITE_OPEN_MAINDB_READONLY #1555

Description

@ra1nj

TL;DR (English): Every normal WCDB handle opens with the private flag SQLITE_OPEN_MAINDB_READONLY. VACUUM INTO attaches its destination with the connection's db->openFlags, so the flag propagates to vacuum_db; unixOpen then creates the destination with an O_RDONLY|O_CREAT fd (the flag clears isReadWrite but keeps isCreate), and the first write lock on it fails deterministically with EBADFSQLITE_IOERR_LOCK "disk I/O error". Net effect: VACUUM INTO fails on every database that is already in WAL mode when the connection opens — i.e. every real-world database from its second session onward. Fix PR: Tencent/sqlcipher#8.

环境

  • WCDB 2.1.15(iOS 侧从 v2.1.15 tag 源码构建复现;masterAbstractHandle.cppsqlcipher submodule pin(f049bed)与 tag 相同,机制仍在)
  • iOS 真机 + 模拟器均 100% 复现

根因链

  1. 常规 handle 主库只读:AbstractHandle::open()READWRITE|CREATE|MAINDB_READONLY(0x100006)打开连接(src/common/core/sqlite/AbstractHandle.cpp:113)。设计意图可以理解:WAL 下写都进 -wal,只有 checkpoint 等专用 slot(InnerDatabase::setupHandle 里的 AutoTask/Assemble/Vacuum)才需要可写主库 fd。
  2. flag 存活到 ATTACH:openDatabase 的 strip 清单不含该位 → db->openFlags 保留它 → attachFuncflags = db->openFlags(attach.c:147)→ VACUUM INTO 内部的 ATTACH %Q AS vacuum_db(vacuum.c:214)把 flag 传给了目标文件。
  3. O_RDONLY|O_CREAT:unixOpen(os_unix.c)对该 flag 清 isReadWrite保留 isCreate → 目标文件以 O_RDONLY|O_CREAT 创建成功(文件建出来了,fd 却只读;上游 stock SQLite 的 assert(isCreate==0 || isReadWrite) 在 fork 里被注释掉了)。
  4. EBADF:ATTACH 读空 schema 成功(F_RDLCK 在只读 fd 上合法);VACUUM 拷贝开写事务拿 RESERVED 锁 → fcntl(F_WRLCK) 打在只读 fd 上,Linux/Darwin 内核都强制返回 EBADFsqliteErrorFromPosixError(EBADF, SQLITE_IOERR_LOCK):
Code:IOError  ExtCode:3850 (SQLITE_IOERR_LOCK)  Message:"disk I/O error"  SystemErrno:9 (EBADF)
  1. 为什么常规测试测不到(伪阴性):全新库首个连接的 BasicConfig 要执行 PRAGMA journal_mode=WAL(写主库)→ 在只读主库 fd 上失败 → 触发 InnerHandle::open 的回退(enableWriteMainDB(true) + 重开)→ 该连接全程可写,VACUUM INTO 成功。所以"新建库 → 立即 VACUUM INTO"的测试全部通过;而已是 WAL 的库(真实用户库的第二个 session 起)100% 失败。

复现步骤

  1. 用 WCDB 打开一个库,写入数据(库转为 WAL),关闭;
  2. 重新打开同一个库(此时已是 WAL,可写重开回退不再触发);
  3. 任意 handle 执行 VACUUM INTO '/some/new/path.db'(如 Android handle.preparedWithMainStatement(...));
  4. → 恒定 SQLITE_IOERR_LOCK(3850)+ errno 9(EBADF)。

影响面

  • VACUUM INTO:如上,已 WAL 库上恒失败;
  • 普通 SQL VACUUM:其临时库的 ATTACH '' 同样继承 flag,同样中招(Database::vacuum() 的 Factory 实现不走 SQL VACUUM,故未暴露);
  • 广义:任何通过常规 handle 对 ATTACH 副库的写入都会命中同一失败(副库 fd 只读,pager 却认为可写)。

修复建议

已提最小修复 PR(vacuum 路径,ATTACH 前后 save/clear/restore 该 flag,恢复 stock SQLite 行为):Tencent/sqlcipher#8

如果想根治广义问题,可以考虑在 attach.c 层面对非主库的 ATTACH 清掉该 flag——是否合适请维护者定夺(比如 checkpoint 语义是否依赖 ATTACH 库的只读主 fd)。

附带观察(供排查发布构建,非本 issue 主体)

Android Maven AAR(com.tencent.wcdb:main:2.1.15)实测不复现此问题:运行中进程 /proc/<pid>/fdinfo 显示主库 fd 为 O_RDWR,同库 VACUUM INTO 成功。但对该 AAR 的 libWCDB.so 反汇编显示 AbstractHandle::open0x100006 调用与 unixOpen 的 bit-20 掩码逻辑都在,且无任何清位指令——即发布的 Android 二进制运行时行为与 v2.1.15 tag 源码语义不一致(iOS 从同一 tag 源码构建则忠实复现 bug)。供团队排查 Android 发布管线时参考。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions