EROFS xattr

EROFS 扩展属性

发布于 2026年9月17日

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

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

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

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

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

图解

先看这张图,再读正文

xattr 在 EROFS 里的三类用途

一句话:xattr 就是挂在文件上的标签——文件内容照常存,标签另外挂,互不干扰。

它在 EROFS 里有三类用途,重要性依次是:

用途 一句话 谁在读它
① 传统:安全与权限 SELinux 标签 / POSIX ACL / capabilities / user.* 内核 LSM、VFS、getfattr(1)、Android 系统服务
② EROFS 特有:ishare 指纹(最关键) mkfs 算好内容指纹存进 xattr,用来判断”哪些文件内容相同、可共享页缓存” erofs_xattr_fill_inode_fingerprint()
③ prefix:长前缀编号化 trusted.overlay. 这类前缀存成表,条目只存 1 字节编号 erofs_getxattr(base_index, infix)

⚠️ 最容易低估的是第 ② 条:ishare 的指纹就存在 xattr 里。 镜像里没有 ishare xattrs 时,内核会打印 on-disk ishare xattrs not found. Turning off inode_share. 并整体关闭该特性—— 没有 xattr,ishare 根本活不了。详见 11 专题。

底部那条链讲的是”怎么存”:
mkfs 按出现次数在 inline(独享、局部性好)与 shared(共用、去重)之间二选一,是典型的空间 vs 局部性权衡;
内核侧两种都要会读,靠 erofs_xattr_iter_inline() / _shared() 两个遍历函数 + 上层一个回调完成。

一、特性缘由:为什么 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_info:xattr_prefix_start、xattr_prefix_count、 xattr_prefixes、ishare_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 路径(erofs_listxattr(),xattr.c):

用户态 listxattr(path) / getfattr -d
    ↓ 系统调用
VFS
    ↓ inode->i_op->listxattr
erofs_listxattr(dentry, buffer, size)                【xattr.c】
    │
    ├─① erofs_init_inode_xattrs(inode)               【xattr.c】
    │      └ 读 ibody header → 填 vi->xattr_isize /
    │        xattr_shared_count / xattr_shared_xattrs[]
    │
    ├─② 填好 it:sb / buf / dentry / buffer /
    │        buffer_size / buffer_ofs = 0
    │
    ├─③ erofs_xattr_iter_inline(&it, inode, false)   【xattr.c】★驱动者
    │      ├ erofs_init_metabuf(&it->buf, ...)
    │      ├ it->pos = erofs_iloc(inode) + inode_isize + xattr_header_sz
    │      └ while (remaining) {
    │            it->kaddr = erofs_bread(&it->buf, it->pos, true)
    │            erofs_listxattr_foreach(it)      ← ★回调(每个 entry 一次)
    │                ├ erofs_xattr_prefix(base_index, it->dentry)
    │                └ erofs_xattr_copy_to_buffer(it, ...)  【xattr.c】
    │        }
    │
    ├─④ erofs_xattr_iter_shared(&it, inode, false)   【xattr.c】★驱动者
    │      └ while (i < vi->xattr_shared_count) {
    │            it->pos = erofs_pos(sb, sbi->xattr_blkaddr)
    │                    + vi->xattr_shared_xattrs[i++] * sizeof(__le32)
    │            it->kaddr = erofs_bread(&it->buf, it->pos, true)
    │            erofs_listxattr_foreach(it)      ← ★同一个回调
    │        }
    │
    ├─⑤ erofs_put_metabuf(&it.buf)                  释放元数据游标
    └─⑥ return ret ? ret : it.buffer_ofs

getxattr 路径(erofs_getxattr(),xattr.c)结构完全一样,差别只有三处:

  listxattr getxattr
入口 inode->i_op->listxattr handler->get → erofs_xattr_generic_get() → erofs_getxattr()
it 多填的字段 — it.index(命名空间编号)、it.name = QSTR(name)
回调 erofs_listxattr_foreach() erofs_getxattr_foreach()

另外 getxattr 在 ① 之后多做一步快速排除:

/* 有 xattr_filter 时,先用哈希位图判断"这个名字肯定不存在" */
hashbit = xxh32(name, strlen(name), EROFS_XATTR_FILTER_SEED + index);
hashbit &= EROFS_XATTR_FILTER_BITS - 1;
if (vi->xattr_name_filter & (1U << hashbit))
        return -ENODATA;          /* 连元数据都不用读了 */

记住这条主线:erofs_init_inode_xattrs() 先准备好”有哪些 xattr”, 然后 erofs_xattr_iter_inline() / _shared() 两个驱动者挨个取 entry, 每取一个回调一次 *_foreach() —— 上层只写这一个回调, 两种存储方式的差异被完全封装在驱动者里(就是理念 2 说的”迭代器 + 回调”)。

3.3 共享 xattr 的”提升”

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

内核侧只负责两种都会读。

四、关键结构体

4.1 struct erofs_xattr_iter(xattr.c)

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

struct erofs_xattr_iter {
	struct super_block *sb;
	struct erofs_buf buf;
	erofs_off_t pos;
	void *kaddr;

	char *buffer;
	int buffer_size, buffer_ofs;

	/* getxattr */
	int index, infix_len;
	struct qstr name;

	/* listxattr */
	struct dentry *dentry;
};
字段 用途
sb 所属超级块
buf 元数据游标(02 专题的 erofs_buf),遍历时临时映射页
pos 当前在元数据里的偏移
kaddr 当前映射页的内核虚拟地址
buffer / buffer_size / buffer_ofs 输出缓冲区、容量、当前写入位置
index / infix_len / name getxattr 专用:要找的命名空间编号、长前缀的中缀长度、名字
dentry listxattr 专用:用于取前缀(erofs_xattr_prefix(base_index, it->dentry))

要点:

  • buf 是 erofs_buf —— 遍历元数据时要临时映射页, 用完必须 erofs_put_metabuf()(02 专题强调过的契约)。 看 erofs_getxattr() 末尾:
    ret = erofs_xattr_iter_inline(&it, inode, true);
    if (ret == -ENODATA)
            ret = erofs_xattr_iter_shared(&it, inode, true);
    erofs_put_metabuf(&it.buf);
    return ret ? ret : it.buffer_ofs;
    
  • buffer_ofs 记录写到了哪——xattr 可能很多,需要累积;最终它也是返回值
  • inode 不在结构体里,而是作为参数传给两个遍历函数:
    static int erofs_xattr_iter_inline(struct erofs_xattr_iter *it,
                                       struct inode *inode, bool getxattr);
    static int erofs_xattr_iter_shared(struct erofs_xattr_iter *it,
                                       struct inode *inode, bool getxattr);
    

    第三个参数 getxattr 决定回调走 get 还是 list(erofs_getxattr_foreach / erofs_listxattr_foreach)

  • xattr_id 已消失:shared 条目编号现在由 erofs_init_inode_xattrs() 读进 vi->xattr_shared_xattrs[], 遍历时直接用;xattr_id 只是那个函数里的一个局部变量(见 xattr.c)

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() 把结果拷进用户缓冲区

设计要点:list 与 get 共用遍历器,只换回调—— 这就是理念 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 的文件没问题

有问题。/tmp 是 tmpfs,不支持 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 (93f51579e7df)