Jerry's Blog

Back

TL;DR:Buck2 在 Antares/Dicfuse 挂载上报 SQLite xShmMap I/O error,表象是 SQLite,根因是 libfuse-fs 0.1.8 的 Layer trait 缺少获取元数据的 API,OverlayFS copy-up 失败返回 ENOSYS。升级到 0.1.9 并在 Dicfuse 实现 getattr_with_mapping 后彻底修复。

TL;DR#

  • 现象:Buck2 报 SQLite xShmMap I/O error,只在 Antares 挂载上出现。
  • 根因:libfuse-fs 0.1.8 的 Layer trait 缺少获取元数据的 API,OverlayFS copy-up 失败,返回 ENOSYS。
  • 触发:SQLite WAL 模式创建 .db-shm,目录在只读 lower 层,需要 copy-up。
  • 解决:升级 libfuse-fs 到 0.1.9,并在 Dicfuse 实现 getattr_with_mapping,copy-up 恢复正常。

目录#


问题背景#

我们的项目 Scorpio 实现了一个基于 FUSE 的虚拟文件系统,使用 OverlayFS 架构:

  • Lower Layer (Dicfuse): 只读层,基于 Git 对象的虚拟文件系统
  • Upper Layer (PassthroughFS): 可写层,用于存储修改
  • OverlayFS: 联合文件系统,将两层合并为一个统一的挂载点

依赖的核心库:

  • libfuse-fs: Rust FUSE 库,提供 OverlayFS 实现
  • 版本演进:从 0.1.6 → 0.1.8 → 0.1.9

初步现象#

在 Antares 挂载点上运行 Buck2 构建工具时,遇到神秘错误:

$ buck2 init
Error: Failed to initialize daemon state
Caused by:
    I/O error within the xShmMap method
bash

奇怪的是

  • 简单的文件读写操作正常
  • 只在 Buck2 初始化时失败
  • 错误信息指向 SQLite 的 xShmMap 方法

调查过程#

第一层:SQLite 错误#

什么是 xShmMap?#

xShmMap 是 SQLite VFS (Virtual File System) 接口的一个方法,用于创建和映射共享内存文件

SQLite 的 WAL (Write-Ahead Logging) 模式需要三个文件:

database.db       ← 主数据库文件
database.db-wal   ← Write-Ahead Log 文件
database.db-shm   ← 共享内存文件(xShmMap 操作的对象)
plaintext

调用链

Buck2 初始化
  → 创建状态数据库
  → SQLite 启用 WAL 模式
  → xShmMap() 创建 .db-shm 文件
  → open(..., "database.db-shm", O_CREAT) 系统调用
  → ❌ 失败!
plaintext

验证假设#

使用 strace 追踪系统调用:

strace -e trace=open,openat,create buck2 init 2>&1 | grep -E "shm|ENOSYS"
bash

发现:

openat(AT_FDCWD, "daemon-state.db-shm", O_RDWR|O_CREAT, 0644) = -1 ENOSYS (Function not implemented)
plaintext

关键发现:不是 SQLite 的问题,而是文件创建失败,返回了 ENOSYS 错误!

第二层:FUSE 文件系统#

为什么文件创建会失败?#

在 FUSE 挂载点上创建文件时:

应用层(Buck2/SQLite)
  ↓ open() 系统调用
Linux 内核 VFS
  ↓ FUSE 协议
FUSE 用户态驱动(OverlayFS)
  ↓ 处理文件创建请求
文件系统实现(Dicfuse/PassthroughFS)
plaintext

问题定位

  • 简单文件创建能成功 → FUSE 协议没问题
  • 只有特定场景失败 → 某种边界条件?

什么场景特殊?#

使用 ls -la 查看挂载点:

$ ls -la /mnt/antares/
total 0
drwxr-xr-x  2 user user    0 Dec 17 10:00 .
drwxr-xr-x  3 root root 4096 Dec 17 09:00 ..
-rw-r--r--  1 user user  123 Dec 17 10:00 existing_file.txt  # 来自 lower layer (Dicfuse)
bash

关键点:Buck2 尝试在已存在于 lower layer 的目录中创建新文件!

第三层:OverlayFS Copy-up#

什么是 Copy-up?#

OverlayFS 的核心机制:当尝试修改只读层(lower layer)的内容时,需要先将其复制到可写层(upper layer)。

┌─────────────────────────────────────┐
│   Upper Layer (可写)                │
│   - PassthroughFS                    │
│   - 存储所有修改                     │
│   - 初始为空                         │
└─────────────────────────────────────┘
          ↑ Copy-up
          │ (从只读层复制到可写层)

┌─────────────────────────────────────┐
│   Lower Layer (只读)                 │
│   - Dicfuse (Git 对象)              │
│   - 不可修改                         │
└─────────────────────────────────────┘
plaintext

触发条件

  1. 修改 lower layer 中的文件
  2. 在 lower layer 的目录中创建新文件 ← Buck2 的场景!
  3. 删除 lower layer 中的文件(创建 whiteout)

Copy-up 需要什么?#

要正确复制文件/目录,必须获取完整的元数据:

struct stat64 {
    st_mode: u32,     // 文件类型和权限 ← 必须!
    st_uid: u32,      // 所有者 UID ← 必须!
    st_gid: u32,      // 所有者 GID ← 必须!
    st_size: i64,     // 文件大小 ← 必须!
    st_atime: i64,    // 访问时间
    st_mtime: i64,    // 修改时间
    // ...
}
rust

问题来了:OverlayFS 如何从 lower layer 获取这些信息?

查看 OverlayFS 源码#

libfuse-fs 中找到 copy_regfile_up 方法:

// libfuse-fs/src/unionfs/overlayfs.rs
async fn copy_regfile_up(&self, req: Request, oi: Arc<OverlayInode>) -> io::Result<()> {
    // 获取 lower layer 文件的元数据
    let (stat, _) = oi.first_layer
        .getattr_with_mapping(oi.first_inode, None, false)  // ← 关键调用!
        .await?;
    
    // 使用元数据创建 upper layer 文件
    let fd = self.do_open(
        req,
        oi.parent,
        &oi.name,
        libc::O_CREAT | libc::O_WRONLY,
        stat.st_mode,  // 使用 lower layer 的权限
    ).await?;
    
    // 设置所有者
    fchown(fd, stat.st_uid, stat.st_gid)?;
    
    // 复制文件内容
    // ...
}
rust

发现核心调用first_layer.getattr_with_mapping()

这是 Layer trait 的方法,用于获取 lower layer 的文件元数据。

第四层:Trait API 缺失#

查看 Layer Trait 定义#

libfuse-fs 0.1.9 中:

#[async_trait]
pub trait Layer: Send + Sync {
    fn root_inode(&self) -> Inode;
    
    async fn lookup(&self, ...) -> Result<...>;
    async fn getattr(&self, ...) -> Result<...>;
    
    // ✅ 新增的方法!
    async fn getattr_with_mapping(
        &self,
        _inode: Inode,
        _handle: Option<u64>,
        _mapping: bool,
    ) -> std::io::Result<(libc::stat64, Duration)> {
        Err(std::io::Error::from_raw_os_error(libc::ENOSYS))  // 默认实现
    }
}
rust

关键点

  • getattr_with_mapping 方法定义
  • 提供默认实现(返回 ENOSYS 错误)
  • 需要各 Layer 实现者覆盖这个方法

检查 Dicfuse 实现#

在 Scorpio 的 src/dicfuse/mod.rs 中:

#[async_trait]
impl Layer for Dicfuse {
    fn root_inode(&self) -> Inode { 1 }
    
    async fn lookup(&self, ...) -> Result<...> { /* 实现 */ }
    async fn getattr(&self, ...) -> Result<...> { /* 实现 */ }
    
    // ❌ 没有实现 getattr_with_mapping!
}
rust

问题定位

  • Dicfuse 没有覆盖 getattr_with_mapping 的默认实现
  • 当 OverlayFS 调用这个方法时,会执行默认实现
  • 默认实现返回 ENOSYS
  • Copy-up 失败!

错误传播链#

完整的错误传播路径:

Buck2 SQLite 尝试创建 .db-shm 文件

FUSE 内核模块收到 FUSE_CREATE 请求

OverlayFS::create() 处理请求

检测到需要 copy-up(目录在 lower layer)

调用 copy_regfile_up() 或 create_upper_dir()

调用 lower_layer.getattr_with_mapping(inode, None, false)

Dicfuse 没有实现 → 使用默认实现

返回 Err(ENOSYS)  ← 错误源头!

Copy-up 失败

文件创建失败,返回 ENOSYS 给内核

内核返回错误给 SQLite

SQLite 包装为 "xShmMap I/O error"

Buck2 看到错误并报告给用户
plaintext

误导性:用户看到的错误(xShmMap)和真正的根因(getattr 未实现)相隔了好几层!

为什么 0.1.8 版本也失败?#

用户提出疑问:

“0.1.8 版本没有 getattr_with_mapping 这个函数,是 do_getattr_helper。那我在 Dicfuse 实现 do_getattr_helper 不就行了吗?“

尝试验证#

创建脚本 scripts/implement_and_test_0.1.8.sh,尝试在 0.1.8 下实现该方法:

#!/bin/bash
# 1. 切换到 libfuse-fs 0.1.8
sed -i 's/libfuse-fs = "0.1.9"/libfuse-fs = "0.1.8"/' Cargo.toml

# 2. 在 Dicfuse 中添加 do_getattr_helper 实现
# 3. 编译
cargo build
bash

意外的发现#

编译失败!错误信息:

error[E0407]: method `do_getattr_helper` is not a member of trait `Layer`
  --> scorpio/src/dicfuse/mod.rs:101:5
   |
101 | /     async fn do_getattr_helper(
102 | |         &self,
103 | |         inode: Inode,
104 | |         _handle: Option<u64>,
...   |
187 | |         Ok((stat, std::time::Duration::from_secs(2)))
188 | |     }
    | |_____^ not a member of trait `Layer`
plaintext

震惊的结论:libfuse-fs 0.1.8 的 Layer trait 根本就没有 do_getattr_helper 方法定义!

验证 libfuse-fs 源码#

克隆 libfuse-fs 仓库并检查:

git clone https://github.com/DavidLiRemini/libfuse-fs.git
cd libfuse-fs

# 检查 0.1.8
git checkout v0.1.8
grep -A 30 "pub trait Layer" src/unionfs/layer.rs
bash

结果:

// 0.1.8 版本
pub trait Layer: Send + Sync {
    fn root_inode(&self) -> Inode;
    async fn lookup(&self, ...) -> Result<...>;
    async fn getattr(&self, ...) -> Result<...>;
    // ... 其他方法
    
    // ❌ 没有 do_getattr_helper
    // ❌ 没有 getattr_with_mapping
}
rust
# 检查 0.1.9
git checkout v0.1.9
grep -A 30 "pub trait Layer" src/unionfs/layer.rs
bash

结果:

// 0.1.9 版本
pub trait Layer: Send + Sync {
    fn root_inode(&self) -> Inode;
    async fn lookup(&self, ...) -> Result<...>;
    async fn getattr(&self, ...) -> Result<...>;
    
    // ✅ 新增的方法!
    async fn getattr_with_mapping(
        &self,
        _inode: Inode,
        _handle: Option<u64>,
        _mapping: bool,
    ) -> std::io::Result<(libc::stat64, Duration)> {
        Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
    }
}
rust

根本原因#

Trait 定义 vs Trait 实现#

这是问题的核心:不是 Dicfuse 没有实现方法,而是 libfuse-fs 的 Layer trait 根本没有定义这个方法!

Rust Trait 机制(三步缺一不可)#

第 1 步:在库中定义 Trait 方法
┌─────────────────────────────────────┐
│ libfuse-fs                          │
│ pub trait Layer {                   │
│   async fn getattr_with_mapping(   │
│     ...                             │
│   ) -> Result<...>;                 │ ← 必须先定义!
│ }                                    │
└─────────────────────────────────────┘

第 2 步:在应用中实现 Trait 方法
┌─────────────────────────────────────┐
│ Scorpio                             │
│ impl Layer for Dicfuse {            │
│   async fn getattr_with_mapping(   │
│     ...                             │
│   ) -> Result<...> {                │ ← 才能实现
│     // 你的代码                     │
│   }                                  │
│ }                                    │
└─────────────────────────────────────┘

第 3 步:在 OverlayFS 中调用
┌─────────────────────────────────────┐
│ OverlayFS                           │
│ let stat = lower_layer              │
│   .getattr_with_mapping(...)        │ ← 才能调用
│   .await?;                          │
└─────────────────────────────────────┘
plaintext

如果第 1 步就没有定义,第 2 步和第 3 步都无法进行!

为什么不能”自己加一个方法”?#

你可能会想:我直接在 Dicfuse 中添加一个普通方法不就行了?

impl Dicfuse {
    // 不通过 trait,直接加个方法
    pub async fn do_getattr_helper(...) -> Result<...> {
        // 我的实现
    }
}
rust

问题

  1. ❌ 这不是 Layer trait 的方法
  2. ❌ OverlayFS 持有的是 Arc<dyn Layer>,不是 Arc<Dicfuse>
  3. ❌ OverlayFS 只能调用 Layer trait 中定义的方法
  4. ❌ 无法通过动态分发(dynamic dispatch)调用具体类型的独有方法
// OverlayFS 中的代码
let lower_layer: Arc<dyn Layer> = Arc::new(dicfuse);
lower_layer.do_getattr_helper(...);  // ❌ 编译错误!
                                     // Layer trait 没有这个方法

// 即使尝试强制转换
let dicfuse_ref = lower_layer.downcast_ref::<Dicfuse>();  
// ❌ Arc<dyn Trait> 无法 downcast
rust

Git 提交历史的线索#

查看 Scorpio 的提交历史:

git log --oneline --grep="getattr"
bash

发现两个关键提交:

提交 feaa21fc - 移除了 do_getattr_helper#

git show feaa21fc
bash
- async fn do_getattr_helper(
-     &self,
-     inode: Inode,
-     _handle: Option<u64>,
- ) -> std::io::Result<(libc::stat64, std::time::Duration)> {
-     // ... 47 行实现代码
- }
diff

提交信息:"not a required member of trait Layer"

误判:开发者认为这不是必需的方法,就删除了。实际上这是 OverlayFS copy-up 的关键功能!

提交 82f79138 - 添加了 getattr_with_mapping#

git show 82f79138
bash
+ async fn getattr_with_mapping(
+     &self,
+     inode: Inode,
+     _handle: Option<u64>,
+     mapping: bool,
+ ) -> std::io::Result<(libc::stat64, std::time::Duration)> {
+     // ... 实现代码(复用了老逻辑)
+ }
diff

提交信息:"fix: implement getattr_with_mapping for libfuse-fs 0.1.9"

完整的时间线#

某个早期版本:
  libfuse-fs 0.1.6
  Scorpio Dicfuse 有 getattr 相关实现
  ✅ 工作正常

↓ (升级到 0.1.8)

libfuse-fs 0.1.8 时期:
  ❌ Layer trait 没有 do_getattr_helper 或类似方法定义
  ❌ OverlayFS copy-up 机制不完整或使用其他方式
  
Scorpio 项目:
  提交 feaa21fc: 移除了 do_getattr_helper 实现
  → "不是 trait 必需的方法"(误判!)
  ❌ Buck2 SQLite xShmMap 错误出现

↓ (升级到 0.1.9)

libfuse-fs 0.1.9:
  ✅ Layer trait 新增 getattr_with_mapping 方法
  ✅ OverlayFS copy-up 完善
  
Scorpio 项目:
  提交 82f79138: 实现了 getattr_with_mapping
  ✅ Buck2 正常工作
plaintext

真正的根因#

libfuse-fs 架构演进

  • 0.1.8 时期:Layer trait 缺少获取元数据的标准 API,OverlayFS copy-up 功能不完整
  • 0.1.9 时期:新增 getattr_with_mapping API,完善 OverlayFS copy-up 机制

Scorpio 的错误

  • 误删了关键实现(feaa21fc)
  • 当时可能因为 0.1.8 的 trait 确实没有这个方法定义,编译器提示”不需要”
  • 但实际上这导致后续升级时缺少必需的功能

解决方案#

1. 升级 libfuse-fs#

修改 Cargo.toml

[dependencies]
libfuse-fs = "0.1.9"  # 从 0.1.8 升级
toml

2. 实现 getattr_with_mapping#

src/dicfuse/mod.rs 中:

#[async_trait]
impl Layer for Dicfuse {
    // ... 其他方法 ...

    /// Retrieve metadata with optional ID mapping control.
    async fn getattr_with_mapping(
        &self,
        inode: Inode,
        _handle: Option<u64>,
        mapping: bool,
    ) -> std::io::Result<(libc::stat64, std::time::Duration)> {
        tracing::debug!(
            "[Dicfuse::getattr_with_mapping] inode={}, mapping={}",
            inode, mapping
        );
        
        // 从 DictionaryStore 获取文件信息
        let item = self.store.get_inode(inode).await
            .map_err(|_| std::io::Error::from_raw_os_error(libc::ENOENT))?;
        
        let attr = item.get_stat().attr;
        let size = if item.is_dir() {
            0
        } else {
            self.store.get_file_len(inode) as i64
        };
        
        // 构造 file type
        let type_bits: libc::mode_t = match attr.kind {
            rfuse3::FileType::Directory => libc::S_IFDIR,
            rfuse3::FileType::Symlink => libc::S_IFLNK,
            _ => libc::S_IFREG,
        };
        
        // 构造 permissions
        let perm: libc::mode_t = if item.is_dir() {
            attr.perm as libc::mode_t
        } else if self.store.is_executable(inode) {
            0o755
        } else {
            0o644
        };
        
        let mode = type_bits | perm;
        let nlink = if attr.nlink > 0 { attr.nlink } else { 1 };
        
        // 构造 stat64 结构
        let mut stat: libc::stat64 = unsafe { std::mem::zeroed() };
        stat.st_ino = inode;
        stat.st_nlink = nlink as _;
        stat.st_mode = mode;
        stat.st_uid = attr.uid;
        stat.st_gid = attr.gid;
        stat.st_size = size;
        stat.st_blksize = 4096;
        stat.st_blocks = (size + 511) / 512;
        stat.st_atime = attr.atime.sec;
        stat.st_atime_nsec = attr.atime.nsec.into();
        stat.st_mtime = attr.mtime.sec;
        stat.st_mtime_nsec = attr.mtime.nsec.into();
        stat.st_ctime = attr.ctime.sec;
        stat.st_ctime_nsec = attr.ctime.nsec.into();
        
        tracing::debug!(
            "[Dicfuse::getattr_with_mapping] Success: mode={:#o}, size={}",
            stat.st_mode, stat.st_size
        );
        
        Ok((stat, std::time::Duration::from_secs(2)))
    }
}
rust

3. 验证修复#

# 构建项目
cargo build

# 挂载 Antares
antares mount /mnt/antares

# 测试 Buck2
cd /mnt/antares/project
buck2 init
# ✅ 成功!

# 检查生成的文件
ls -la .buck/
# daemon-state.db
# daemon-state.db-wal
# daemon-state.db-shm  ← 成功创建!
bash

如何复现 & 验证已修复#

复现实验和对照验证放在一起,方便快速重放。

复现(Buggy 路径,libfuse-fs 0.1.8)

  • 修改 Cargo.toml 指定 libfuse-fs = "0.1.8",或直接运行脚本 scripts/implement_and_test_0.1.8.sh(其中会尝试实现旧方法并编译,编译错误即为证据)。
  • 启动 Antares 并挂载到 /mnt/antares
  • 运行 buck2 init,或 sqlite3 test.db "CREATE TABLE t(id INTEGER);"
  • 预期:看到 xShmMap I/O error;strace 中可见 .db-shm 创建返回 ENOSYS。

验证修复(Healthy 路径,libfuse-fs 0.1.9 + 正确实现)

  • 使用 libfuse-fs = "0.1.9",并确保 Dicfuse 实现了 getattr_with_mapping
  • 相同挂载、相同命令:buck2 initsqlite3 test.db ...
  • 预期:.db-shm 文件存在;日志中能看到成功的 getattr_with_mapping 调用;无 xShmMap 错误。
  • 可用脚本/测试:scripts/implement_and_test_0.1.8.sh(对比编译行为)、tests/test_copy_up_chain.rs

关键洞察#

1. 错误信息的误导性#

用户看到的错误层级:
┌────────────────────────────────────┐
│ Layer 5: Buck2 SQLite xShmMap error │ ← 表象
├────────────────────────────────────┤
│ Layer 4: SQLite WAL 初始化失败      │
├────────────────────────────────────┤
│ Layer 3: xShmMap() 系统调用失败     │
├────────────────────────────────────┤
│ Layer 2: FUSE 文件创建失败          │
├────────────────────────────────────┤
│ Layer 1: OverlayFS copy-up 失败     │
├────────────────────────────────────┤
│ Layer 0: getattr 方法未实现         │ ← 根因
└────────────────────────────────────┘
plaintext

教训:不要被表面错误信息迷惑,要层层深入找到真正的根因。

2. Trait 定义的重要性#

在 Rust 中使用 trait 对象(Arc<dyn Trait>)时:

  • 只能调用 trait 中定义的方法
  • 如果 trait 没有定义该方法,即使具体类型实现了,也无法通过 trait 对象调用
  • 这是编译时就确定的,无法运行时 downcast

教训

  • 理解 Rust trait 的编译时多态(静态分发)和运行时多态(动态分发)
  • 使用 trait 对象时,trait 定义就是接口契约
  • 不能在具体类型中”偷偷”添加方法来绕过 trait

3. 默认实现的陷阱#

pub trait Layer {
    async fn getattr_with_mapping(...) -> Result<...> {
        Err(ENOSYS)  // 默认实现返回错误
    }
}
rust
  • 默认实现让 trait 可以向后兼容地添加新方法
  • 但如果默认实现返回错误,容易被忽视
  • 各实现者必须主动覆盖默认实现

教训

  • 检查 trait 的所有方法,不要遗漏默认实现
  • 使用 #[warn(unused_trait_methods)] 等工具辅助检查
  • API 文档应明确说明哪些默认实现必须覆盖

4. Copy-up 机制的复杂性#

OverlayFS 的 copy-up 需要:

  1. 获取 lower layer 的完整元数据(权限、所有者、大小等)
  2. 在 upper layer 创建文件
  3. 复制文件内容
  4. 保持元数据一致性

任何一步失败都会导致整个操作失败。

教训

  • 理解你使用的文件系统架构
  • OverlayFS 不是简单的”合并”,有复杂的 copy-up 语义
  • 测试时要覆盖”跨层”操作的场景

5. SQLite WAL 模式的特殊性#

WAL 模式需要创建额外的文件(.db-wal, .db-shm),这些文件:

  • 必须与主数据库在同一目录
  • 需要特定的权限和所有者
  • 如果创建失败,整个数据库初始化失败

教训

  • SQLite 不只是操作一个文件,WAL 模式需要多文件协调
  • 文件系统必须支持完整的文件创建语义
  • 测试数据库相关功能时,要考虑 WAL/Journal 等模式

经验教训#

1. 不要轻易删除”看起来不必要”的代码#

feaa21fc 提交删除了 47 行代码,理由是”不是 trait 必需的方法”。

反思

  • 虽然编译器说”不必需”,但可能是功能性必需
  • 删除前要理解代码的业务逻辑和使用场景
  • 运行完整的测试套件,包括集成测试

2. 依赖库升级要谨慎#

从 0.1.8 升级到 0.1.9 时:

  • API 发生变化(do_getattr_helpergetattr_with_mapping
  • 语义可能变化(新增 mapping 参数)
  • 必须仔细阅读 Changelog 和 Migration Guide

建议

  • 使用 cargo-semver-checks 等工具检测 API 变化
  • 为关键功能编写集成测试
  • 在测试环境充分验证后再升级生产环境

3. 调试要有系统性#

遇到问题时的调试流程:

  1. 收集现象(错误信息、日志、系统调用追踪)
  2. 提出假设(哪一层出问题?)
  3. 设计实验(strace、编译测试、单元测试)
  4. 验证假设(逐步缩小范围)
  5. 找到根因(不要停留在表象)

工具箱

  • strace: 系统调用追踪
  • ltrace: 库函数调用追踪
  • gdb: 调试器
  • cargo expand: 展开宏
  • cargo-semver-checks: API 兼容性检查
  • 单元测试、集成测试
  • 日志和 tracing

4. 文档和知识沉淀#

这次调试花费了大量时间,如果没有文档沉淀:

  • 其他人遇到类似问题还要从头调试
  • 几个月后自己可能也忘记了细节
  • 团队知识无法传承

建议

  • 重要问题写技术复盘(就像本文)
  • 维护 FAQ 文档
  • 在代码中添加详细注释,说明”为什么”
  • 提交信息要清晰,说明背景和动机

常见问题快速回顾(FAQ )#

  • Q1:为什么 libfuse-fs 0.1.8 会失败?

    • A:因为 0.1.8 的 Layer trait 根本没有提供获取 lower layer 元数据的 API(既没有 do_getattr_helper,也没有 getattr_with_mapping),OverlayFS 无法完成 copy-up,导致所有写相关操作在边界场景下失败,最终表现为 Buck2 / SQLite 的 xShmMap 错误。
  • Q2:在 Dicfuse 里实现一个 do_getattr_helper 方法能不能救回来?

    • A:不能。原因有两层:
      • 编译层面:trait 根本没这个方法定义,impl Layer for Dicfuse 里实现它会直接触发 error[E0407]: method 'do_getattr_helper' is not a member of trait 'Layer'
      • 架构层面:OverlayFS 通过 Arc<dyn Layer> 调用方法,只能调用 trait 定义的接口;就算在 impl Dicfuse 里加了普通方法,OverlayFS 也完全看不到。
  • Q3:Buck2 SQLite xShmMap 错误和 OverlayFS copy-up 的关系到底有多直接?

    • A:链路可以精炼成一句话:“获取不到 lower 元数据 → copy-up 失败 → .db-shm 创建失败 → xShmMap 失败 → Buck2 初始化失败”
      如果想看完整路径,可以对照文中那张“错误传播图”和 doc/SQLITE_XSHMMAP_AND_COPYUP.md 里的分层分析。
  • Q4:为什么升级到 libfuse-fs 0.1.9 就好了,看起来只是函数名换了下?

    • A:不是简单重命名,而是新增了一个在 trait 上正式定义的 API
      • 0.1.9 在 Layer trait 中新增了 getattr_with_mapping,OverlayFS 的 copy-up 逻辑也统一改为调用这个方法。
      • 这迫使所有 Layer 实现者(包括 Dicfuse)必须实现它,否则项目根本无法编译。
      • 一旦我们在 Dicfuse 里给出正确实现,OverlayFS 就终于拿到了完整的 stat64,copy-up 与 SQLite / Buck2 随之恢复正常。
  • Q5:如果当年在 0.1.8 时代就有一个对应的 trait 方法,会怎样?

    • A:如果 0.1.8 的 Layer trait 中本来就定义了 do_getattr_helper,而 Dicfuse 也一直保留实现,那么这次 bug 很大概率根本不会出现。
      真正的问题是:trait 层没有定义 + 实现层误删逻辑 + 缺乏端到端测试 三个因素叠加。
  • Q6:这次复盘中最“硬”的证据是什么?

    • A
      • 编译错误:method 'do_getattr_helper' is not a member of trait 'Layer'(来自验证脚本 scripts/implement_and_test_0.1.8.sh)。
      • git show 证明:老的 Dicfuse 实现确实曾存在并在 feaa21fc 中被整体删除。
      • 对比 0.1.8 / 0.1.9 Layer trait 源码,确认新版本才引入了 getattr_with_mapping
      • 在 0.1.9 + 正确实现下,Buck2 / SQLite 场景稳定通过。

未来工作 / 改进方向#

  • 增加针对 copy-up 的集成测试覆盖 SQLite / Buck2 场景,防止回归。
  • 在 CI 增加一个 Buck2 / SQLite 的 smoke test(含挂载环境),至少跑最小用例。
  • Layer 这类关键 trait 方法加检查(lint/自检脚本),避免落回默认 ENOSYS。
  • 持续向上游反馈:API 文档、默认实现的提示,以及可能的示例实现。

总结#

这次调试从一个神秘的 “SQLite xShmMap I/O error” 出发,层层深入:

  1. SQLite WAL 模式需要创建共享内存文件
  2. 文件创建触发 FUSE 系统调用
  3. FUSE OverlayFS 需要进行 copy-up 操作
  4. Copy-up 需要从 lower layer 获取文件元数据
  5. 获取元数据需要调用 Layer trait 的方法
  6. libfuse-fs 0.1.8 的 Layer trait 根本没有定义这个方法

最终找到根因:不是实现问题,而是 API 定义缺失。

解决方案:升级到 libfuse-fs 0.1.9 并实现 getattr_with_mapping 方法。

关键洞察

  • 错误信息可能有很强的误导性,要追根溯源
  • Rust trait 的定义和实现有本质区别
  • 理解底层机制(OverlayFS、FUSE、SQLite WAL)很重要
  • 系统性的调试方法和工具链很关键
  • 文档和知识沉淀能让团队受益

希望这篇复盘能帮助遇到类似问题的开发者快速定位问题,也能启发大家在调试复杂问题时的思路。


相关资源

调试工具代码

深度复盘:Buck2 SQLite xShmMap 与 OverlayFS Copy-up 故障
https://jerry609.github.io/blog/buck2-sqlite-xshmmap-copyup
Author Jerry
Published at December 17, 2025
Comment seems to stuck. Try to refresh?✨