Rust in Everything
基于 tantivy 0.27.0

别的用户读不了索引:Permission denied

主线位置:磁盘主线第一步,目录里的 meta.json、.managed.json 和两个锁文件。每个文件是什么见一个段,atomic_write 和 ManagedDirectory 见 Directory。

flowchart TB
  cm["commit"] --> aw["atomic_write"]
  aw --> tf["tempfile 0600"]
  tf --> mv["改名为 meta.json"]
  mv --> other["别的用户 open"]
  other --> e1["读 meta.json 拒绝"]
  other --> e2["写 meta.lock 拒绝"]

实验 crate:exp-tantivy/pita-lab,src/bin/p14_perm.rs:create 建索引并提交一次,commit 再提交一次,open 做 Index::open_in_dir 加 reader()。本机只有一个普通用户,「别的用户读不到」用 chmod 拿掉属主自己的权限来模拟,得到的是同一个 EACCES。M2 Max、macOS 27、rustc 1.99.0,release,umask 022。

现象

建索引后的权限,以及手动放开、再提交一次:

umask 022
created:
  600 .managed.json, 600 meta.json, 644 fd83143ea39847f182f5b1a7a1b4ad09.store
$ chmod 644 meta.json .managed.json
one more commit:
  600 .managed.json, 600 meta.json, 644 2c76ed9ba407477e821967009d0f4938.store, 644 fd83143ea39847f182f5b1a7a1b4ad09.store
  • 段文件按 umask 是 0644,meta.json 和 .managed.json 却是 0600。
  • 手动 chmod 644 只管到下一次提交:提交后两个文件又是 0600。

读不了这两个文件时打开索引的报错:

$ chmod 000 meta.json
open_in_dir + reader: Err(OpenReadError(IoError { io_error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }, filepath: "meta.json" }))
$ chmod 600 meta.json; chmod 000 .managed.json
open_in_dir + reader: Err(OpenReadError(IoError { io_error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }, filepath: ".managed.json" }))

典型场景:索引由一个服务账号写,另一个账号下的运维脚本、备份校验程序、或者以不同用户跑的查询服务去读。

原因

0600 来自 tempfile。 meta.json 和 .managed.json 都走 atomic_write:在同一目录建临时文件、写完、sync_data、改名:

    let mut tempfile = tempfile::Builder::new().tempfile_in(parent_path)?;
    tempfile.write_all(content)?;
    tempfile.flush()?;
    tempfile.as_file_mut().sync_data()?;
    tempfile.into_temp_path().persist(path)?;

tempfile 3.27 建临时文件的默认权限是 0o600(它的 Builder::permissions 文档:「Permissions default to 0o600 for tempfiles」),tantivy 没有改它。改名保留 inode 和权限,所以每次提交都是一个新的 0600 文件,之前的 chmod 随旧文件一起被替换掉。段文件走 open_write,是普通的创建,所以按 umask 是 0644。

reader 还要写一个锁文件。 即使 meta.json 能读,reader 打开段时要拿 META_LOCK(open_segment_readers,防止 GC 在打开过程中删文件),而 MmapDirectory::acquire_lock 是以写方式打开 .tantivy-meta.lock、不存在就创建。这个文件按 umask 是 0644,别的用户没有写权限:

-rw-r--r--   1 user staff     0 Oct 29 18:04 .tantivy-meta.lock
$ chmod 444 .tantivy-meta.lock
open_in_dir + reader: Err(LockFailure(IoError(Os { code: 13, kind: PermissionDenied, message: "Permission denied" }), None))

所以另一个用户要读这个索引,三样都得有:meta.json、.managed.json 可读,.tantivy-meta.lock 可写(不存在时还要能在目录里创建它)。tantivy 的设计假设是读写索引的都是同一个用户。0.27.0 之后 main 上没有改 src/directory/。

📝NOTE
冷知识:atomic_write 从 0.14 起用 tempfile

0.14 之前,MmapDirectory::atomic_write 用的是 atomicwrites crate 的 AtomicFile。PR #866(31d18dca1,「Removing dependency to atomicwrites」)把它换成 tempfile::NamedTempFile::new() 加 persist,CHANGELOG 0.14.0 记作「Remove dependency to atomicwrites #833」。NamedTempFile::new() 建在系统临时目录里,而改名不能跨文件系统;两周后 PR #878(151498cbe,「Creating the tempfile for atomicwrites in the same directory as the MmapDirectory.」)改成 tempfile::Builder::new().tempfile_in(...),也就是今天这段代码的写法。两个提交都首次发布在 0.14。从那以后 meta.json 就是 tempfile 建的文件,权限跟着 tempfile 走。

修法

最简单:读写索引的进程用同一个用户。 查询服务、运维脚本都切到写索引的那个账号。

确实要给另一个用户读: 包一层 Directory,只改 atomic_write,让临时文件以 0644 创建,其余方法全部转发给 MmapDirectory:

#[derive(Clone, Debug)]
struct ShareableDir(MmapDirectory, std::path::PathBuf);

impl Directory for ShareableDir {
  fn atomic_write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
    let mut tmp = tempfile::Builder::new()
      .permissions(std::fs::Permissions::from_mode(0o644))
      .tempfile_in(&self.1)?;
    tmp.write_all(data)?;
    tmp.as_file_mut().sync_data()?;
    tmp.into_temp_path().persist(self.1.join(path))?;
    Ok(())
  }
  // get_file_handle, delete, exists, open_write, atomic_read, sync_directory,
  // acquire_lock, watch: forward to self.0
}

用 Index::create(ShareableDir(..), schema, settings) 建、Index::open(ShareableDir(..)) 打开,写两次:

created (ShareableDir):
  644 .managed.json, 644 8a4588a9ef2343a688d4aec2f3b94010.store, 644 meta.json
one more commit (ShareableDir):
  644 .managed.json, 644 8a4588a9ef2343a688d4aec2f3b94010.store, 644 8f1b92fd98f94392a03d0fb727475049.store, 644 meta.json
open_in_dir + reader: Ok, num_docs 2

acquire_lock 一定要转发给 MmapDirectory:trait 的默认实现是「文件存在即上锁」,而 MmapDirectory 用过的锁文件会一直留在目录里,用默认实现就会一直以为锁被占着(两种实现见 IndexWriter)。另外把 .tantivy-meta.lock 的权限放开一次(比如 chmod 666,或者归到一个共同的组再 g+w):这个文件建好以后不会被删除重建,改一次就一直有效。读者进程用普通的 Index::open_in_dir 就行。

本机只有一个用户,「另一个用户真的能读」没有直接验证;上面验证的是三个文件的权限位,以及缺了任何一个时的报错。

怎么检查

  • ls -la 索引目录:meta.json、.managed.json 是 -rw-------,.tantivy-meta.lock 是 -rw-r--r--,属主不是读者进程的用户,就会中招。
  • 用读者的账号试一次:sudo -u <读者> <程序> open <目录>,或者直接 sudo -u <读者> cat meta.json。
  • 报错里 filepath 是 meta.json 或 .managed.json,或者是 LockFailure(IoError(... PermissionDenied ...)),都是这个问题。

小结

  • meta.json、.managed.json 每次提交都由 tempfile 重建,权限 0600,chmod 只管到下一次提交;段文件是 0644。
  • reader 还要以写方式打开 .tantivy-meta.lock;它是 0644,别的用户写不了。
  • 读写用同一个用户最省事;要共享,就包一层 Directory 让 atomic_write 用 0644,并放开 .tantivy-meta.lock 的写权限。

💬 关联讨论

发起讨论
加载中...