EROFS xattr

EROFS 扩展属性

发布于 2026年9月17日

12 · 特性专题:xattr(扩展属性)

对应配置:CONFIG_EROFS_FS_XATTR(及其子项 _POSIX_ACL / _SECURITY) 主源码:
xattr.cxattr.h 相关:internal.hxattr_isizexattr_blkaddr 等字段)

本文回答:xattr 是什么、EROFS 为什么需要它两种存储方式怎么权衡内核怎么遍历与查找关键结构体与函数用户态如何读到

本专题目标(读完你应该能做到什么)

  1. 说清 xattr 是什么、EROFS 为什么特别依赖它(不止是”扩展属性”那么简单)
  2. 区分 inlineshared 两种存储方式,并说清各自的取舍
  3. 看懂 erofs_xattr_iter 这个”遍历上下文”的作用
  4. 解释 xattr_filter / xattr_prefix_start 这类新机制在解决什么
  5. 知道 ishare 的指纹就存在 xattr 里(与 11 专题的联系)
  6. 能在虚拟机里实际读出 EROFS 镜像的 xattr

图解

先看这张图,再读正文

xattr 在 EROFS 里的三类用途

一、特性缘由:为什么 EROFS 需要 xattr

1.1 xattr 本身:给文件附加”键值对”

xattr(扩展属性)就是挂在文件上的额外信息:

security.selinux = "system_u:object_r:..."
user.mime_type   = "text/plain"

传统用途:

用途 例子
SELinux 安全标签 security.selinux
POSIX ACL system.posix_acl_access
capabilities security.capability
用户自定义 user.*

对只读文件系统一样重要——Android 就大量依赖 security.selinux

1.2 但 EROFS 还有个特殊用途:指纹

这是 EROFS 里 xattr 最关键、也最容易被忽略的用途:

ishare(page cache sharing)的文件指纹存在 xattr 里。

见 11 专题:erofs_xattr_fill_inode_fingerprint() 从 xattr 读出指纹, 用来判断”哪些文件内容相同、可以共享页缓存”。

⇒ 没有 xattr 支持,ishare 根本无法工作

(实测现象:挂载一个没有 ishare xattrs 的镜像并指定 inode_share 时,
内核会打印 on-disk ishare xattrs not found. Turning off inode_share. ——就是这条依赖的直接体现。)

1.3 还有一个用途:xattr prefix(前缀表)

较新的 EROFS 引入了 xattr prefix 机制:把常见的前缀(如 trusted.overlay.) 存成一张表,条目里只存编号,从而省空间

相关字段见 erofs_sb_infoxattr_prefix_startxattr_prefix_countxattr_prefixesishare_xattr_prefix_id

二、设计理念

理念 1:两种存储,按”是否值得共享”选择

EROFS 把 xattr 存成两种形式:

  inline shared
位置 紧跟在 inode 后面 集中在 xattr_blkaddr 指向的元数据块
适用 只被一个 inode 用 多个 inode 共用
优点 局部性好,读 inode 顺带拿到 去重——相同 xattr 只存一份
代价 多份相同 xattr 会浪费空间 多一次跳转

判断依据:mkfs 时统计。若某组 xattr 只出现一次 → inline; 若多次出现 → 提升为 shared。

⇒ 这是典型的空间 vs 局部性权衡。

理念 2:用”迭代器 + 回调”遍历

xattr 的两种存储差异很大,如果每次操作都写两套代码会很啰嗦。

EROFS 的做法:定义 struct erofs_xattr_iter 作为遍历上下文, 配两个”遍历函数”:

  • erofs_xattr_iter_inline() —— 遍历 inline 部分
  • erofs_xattr_iter_shared() —— 遍历 shared 部分

上层(list / get)只写一个回调,由遍历函数负责”怎么挨个取出来”。

理念 3:prefix 换编号,进一步压缩

trusted.overlay. 这类长前缀在容器场景里成千上万次出现。 存成表后,每个条目只存 1 字节编号——省下的空间相当可观。

三、实现架构

3.1 磁盘布局

inode(on-disk)
   ├── 定长部分
   ├── 变长部分(若有)
   ├── xattr 区(inline)        ← xattr_isize 指定大小
   │      └── 多个 entry:name_len / value_size / 后缀名 / 值
   └── ...

元数据区 xattr_blkaddr
   └── shared xattr 条目(按哈希/编号索引)
          └── 多个 inode 指向同一条

3.2 内核读取路径

用户态 listxattr(path)
    ↓ 系统调用
VFS
    ↓ inode->i_op->listxattr
erofs_listxattr()                        【xattr.c】
    ↓
erofs_init_inode_xattrs()               读 inode 的 xattr 区大小/位置
    ↓
erofs_listxattr_foreach(it)             遍历回调
    ↓
erofs_xattr_iter_inline(it)  ──► 遍历 inline 区
erofs_xattr_iter_shared(it)  ──► 遍历 shared 区
    ↓
erofs_xattr_copy_to_buffer()            拷到用户缓冲区

getxattr 路径同理,只是回调换成 erofs_getxattr_foreach()

3.3 共享 xattr 的”提升”

mkfs 侧的行为(不是内核):统计每个 xattr 集合出现次数, 多次出现的写入 shared 区,inode 里只留一个引用编号。

内核侧只负责两种都会读

四、关键结构体

4.1 struct erofs_xattr_iterxattr.c

遍历上下文——贯穿所有 xattr 操作:

struct erofs_xattr_iter {
	struct super_block *sb;
	struct erofs_buf buf;          /* 元数据游标(02 专题的 erofs_buf)*/
	struct inode *inode;

	char *buffer;                  /* 输出缓冲区 */
	int buffer_size, buffer_ofs;   /* 容量与当前写入位置 */

	unsigned int xattr_id;         /* shared 时的编号 */
	...
};

要点:

  • buferofs_buf —— 遍历元数据时要临时映射页, 用完必须 erofs_put_metabuf()(02 专题强调过的契约)
  • buffer_ofs 记录写到了哪——xattr 可能很多,需要累积

4.2 erofs_inode 中的 xattr 字段(internal.h

unsigned int xattr_isize;              /* inline xattr 区大小 */
unsigned int xattr_name_filter;        /* 名字过滤位图 */
unsigned int xattr_shared_count;       /* shared xattr 条目数 */
unsigned int *xattr_shared_xattrs;     /* shared 条目编号数组 */

4.3 erofs_sb_info 中的 xattr 字段(internal.h

u32 xattr_blkaddr;                     /* shared 区起始块 */
u32 xattr_prefix_start;                /* prefix 表起始 */
u8  xattr_prefix_count;                /* prefix 数量 */
u8  ishare_xattr_prefix_id;            /* ishare 指纹用的 prefix id */
struct erofs_xattr_prefix_item *xattr_prefixes;   /* prefix 表 */
unsigned int xattr_filter_reserved;

4.4 struct xattr_handler(内核通用)

EROFS 为不同命名空间注册不同 handler:

handler 命名空间
erofs_xattr_user_handler user.*
erofs_xattr_trusted_handler trusted.*
erofs_xattr_security_handler security.*
(POSIX ACL) system.posix_acl_*

每个 handler 决定”这个命名空间的 xattr 要不要显示/能不能读” (例如 user.* 在非特权场景可隐藏)。

五、主要函数

5.1 遍历与查找

函数 作用
erofs_init_inode_xattrs() 初始化:算出 inode 的 xattr 区在哪、多大
erofs_xattr_iter_inline() 遍历 inline 区的条目
erofs_xattr_iter_shared() 遍历 shared 区的条目
erofs_listxattr_foreach() “列出全部”用的回调
erofs_getxattr_foreach() “取某一个”用的回调
erofs_xattr_copy_to_buffer() 把结果拷进用户缓冲区

设计要点listget 共用遍历器,只换回调—— 这就是理念 2 的落地。

5.2 对 VFS 的接口

函数 作用
erofs_listxattr() 列出某文件的全部 xattr 名
erofs_getxattr() 取某个 xattr 的值

它们注册到 inode_operations / xattr_handler,由 VFS 回调。

5.3 prefix 机制

函数 作用
erofs_xattr_prefixes_init() 挂载时读入 prefix 表
erofs_xattr_prefixes_cleanup() 卸载时释放

六、来龙去脉:一次 getfattr 的完整过程

① mkfs 时:
     ├ 收集每个文件的 xattr
     ├ 只出现一次 → inline(写进 inode 后面)
     └ 出现多次   → shared(写进 xattr_blkaddr 区,inode 记编号)
        │
② 挂载:
     └ erofs_xattr_prefixes_init() 读入 prefix 表
        │
③ 用户态执行 getfattr -d /mnt/f.txt
        │
④ 系统调用 listxattr / getxattr
        │
⑤ VFS → erofs_listxattr() / erofs_getxattr()
        │
⑥ erofs_init_inode_xattrs()
     ├ 读 inode 的 xattr_isize / xattr_shared_count
     └ 算出 inline 区与 shared 编号数组的位置(用 erofs_buf 映射元数据)
        │
⑦ 遍历:
     ├ erofs_xattr_iter_inline()   → 挨个取 inline 条目
     └ erofs_xattr_iter_shared()   → 按编号去 shared 区取
        │
⑧ 每个条目交给回调(foreach)
     └ erofs_xattr_copy_to_buffer() 写进用户缓冲区
        │
⑨ erofs_put_metabuf() 释放元数据映射
        │
⑩ 返回用户态 → getfattr 打印
        user.test1="hello-xattr"
        user.test2="second-value"

七、动手验证

下面的方法都实测过(详见 erofs-analysis/10-EROFS-xattr-用户态读取验证.md)。

验证 1:宿主机侧确认镜像里有 xattr(不用挂载)

/opt/erofs-utils/bin/dump.erofs --path=/f.txt /tmp/erofs-lab/xattr.erofs

看输出的 Xattr size:

  • Xattr size: 104 → 有 xattr
  • Xattr size: 0 → 没有

对照:用 mkfs.erofs -x -1 禁用 xattr 造的镜像,Xattr size: 0

⚠️ dump.erofs 只能看大小,列不出 key=value。 想看内容必须挂载(验证 2、3)。

验证 2:宿主机用 erofsfuse(不需要内核支持)

mkdir -p /mnt/xattr
/opt/erofs-utils/bin/erofsfuse /tmp/erofs-lab/xattr.erofs /mnt/xattr
getfattr -R -d /mnt/xattr
umount /mnt/xattr

验证 3:QEMU 虚拟机里用内核挂载

# VM 内
mount -t erofs /host/xattr.erofs /mnt/x
/host/listxattr /mnt/x/f.txt      # user.test1 / user.test2
/host/listxattr /mnt/x/g.txt      # user.foo

⚠️ VM 里是 busybox 最小系统,没有 getfattr。 需要宿主机静态编译一个 listxattr 丢进共享目录 (gcc -static,源码见 img-stable/test/tools/listxattr.c)。

验证 4:造一个带 xattr 的镜像

# 注意:必须在 ext4 上造,/tmp 是 tmpfs 不支持 user.* xattr!
mkdir -p /sdd/xattrtest/fs
echo "hello" > /sdd/xattrtest/fs/f.txt
setfattr -n user.test1 -v "hello-xattr" /sdd/xattrtest/fs/f.txt
/opt/erofs-utils/bin/mkfs.erofs /sdd/xattrtest/xattr.erofs /sdd/xattrtest/fs

八、常见误解(重要)

误解 1:dump.erofs 能列出 xattr 内容

不能。它只有 -S -e -s --ls --cat --nid --path 这些选项, 没有 xattr 选项,只能显示 Xattr size(大小)。

要看内容必须挂载(erofsfuse 或内核挂载)。

误解 2:xattr 只是一种”附加信息”,可有可无

对 EROFS 不是——ishare 的指纹就存在 xattr 里。 没有 xattr 支持,page cache sharing 直接失效。

误解 3:inline 一定比 shared 好

不一定。inline 局部性好,但相同 xattr 会被复制多份; shared 去重省空间,但多一次跳转。 mkfs 会按出现次数自动选择。

误解 4:在 /tmp 里造带 xattr 的文件没问题

有问题。/tmptmpfs,不支持 user.* xattr, setfattr 会报 Operation not supported(errno 95)。 要用 ext4(如 /sdd

误解 5:VM 里能用 getfattr

不能(busybox 最小系统没带)。要么宿主机静态编译一个工具, 要么用 erofsfuse 在宿主机直接读。

九、与其他特性的关系

特性 关系
ishare / page cache sharing(11 专题) 强依赖——指纹存在 xattr 里
多设备 shared xattr 区也走 erofs_map_dev(),可跨设备
metabox shared xattr 可放在 metabox 里(erofs_sb_has_shared_ea_in_metabox()
POSIX ACL / security 都是 xattr 的特殊命名空间,靠 handler 区分
on-disk 格式(01 专题) xattr_isize 是 inode 变长部分的关键字段

自测检查点

  1. xattr 在 EROFS 里除了传统用途,还有什么关键作用
  2. inline 与 shared 两种存储各自的适用场景与代价?
  3. 谁来决定某个 xattr 用 inline 还是 shared?
  4. erofs_xattr_iter 里的 buf 是什么?用完要做什么?
  5. list 与 get 两个操作如何复用遍历逻辑?
  6. prefix 机制解决什么问题?
  7. 为什么在 /tmp 里造不了 user.* xattr?该去哪造?
  8. dump.erofs 能看到 xattr 的内容吗?为什么?
  9. 想实际读出 EROFS 镜像的 xattr,有哪几条路?
  10. 没有 xattr 支持会影响哪个高级特性?

自测答案

点击展开 1. **ishare 的文件指纹存在 xattr 里**。没有 xattr 支持, `erofs_xattr_fill_inode_fingerprint()` 取不到指纹, page cache sharing 就无法工作。 2. **inline**:只被一个 inode 用 → 紧跟 inode,局部性好,但相同内容会存多份。 **shared**:被多个 inode 共用 → 集中在 `xattr_blkaddr`,去重省空间, 但多一次跳转。 3. **mkfs 决定**(不是内核)。mkfs 统计出现次数: 一次 → inline;多次 → 提升为 shared。 4. `buf` 是 `struct erofs_buf`(元数据游标)。 用完必须调用 `erofs_put_metabuf()` 释放——这是 02 专题强调过的契约。 5. 共用 `erofs_xattr_iter` 遍历器与 `erofs_xattr_iter_inline/shared()`, 只把**回调**换掉:list 用 `erofs_listxattr_foreach()`, get 用 `erofs_getxattr_foreach()`。 6. 把 `trusted.overlay.` 这类重复出现的长前缀存成表, 条目里只存编号,省空间(容器场景前缀重复率极高)。 7. `/tmp` 是 **tmpfs**,不支持 `user.*` xattr(errno 95, `EOPNOTSUPP`)。 应改用 **ext4**,例如 `/sdd`。 8. **不能**。它只有 `-S -e -s --ls --cat --nid --path`,没有 xattr 选项, 只能显示 `Xattr size`(大小)。要看内容必须挂载。 9. 三条: **①** 宿主机 `erofsfuse` 挂载 + `getfattr`(不需内核支持); **②** QEMU 内核挂载 + 静态编译的 `listxattr`; **③** 宿主机 `dump.erofs` 只能看大小(不算真正读出内容)。 10. **ishare / page cache sharing**(11 专题)——指纹依赖 xattr, 没有指纹就无法判断哪些文件内容相同。

参考

linux-stable