复制后的字节内容可以完全一致,但应用需要的扩展属性已经缺失。

扩展属性属于 inode,也受命名空间约束

扩展属性在普通文件内容流之外,为文件系统对象附加有名字的字节值。应用可以使用 user 命名空间存放小型元数据,其他命名空间则具有内核、安全模块或特权相关语义。它是否可靠,取决于文件系统、访问规则和传输工具。只有工作流程按明确契约保留并解释标签,元数据才能成为下游判断的有效依据。 坏掉的样子 属性值不一定是文本,可以包含空字符或任意二进制数据。把它当作以零结尾的字符串,或者假定固定编码,可能破坏原值。空属性和缺失属性也不是同一状态;如果都返回空字符串,就会隐藏元数据到底被刻意设为空,还是在传输中丢失。使用前需要明确编码与缺失语义,而不是依赖显示工具碰巧如何呈现。 同一文件系统内重命名保留对象及属性,复制内容通常却创建另一个 inode。复制工具决定尝试保留哪些元数据,因此文件哈希相同不能证明扩展属性相同。流程可能成功传递每一个内容字节,却遗漏下游需要的分类、来源或应用版本标签。内容校验通过,只能证明它实际覆盖的内容范围。 命名空间名称不会自动建立应用授权规则。设置 user.classification 为 private,不会让本来可读的文件变得保密。user 属性服从适用的文件访问权限,安全属性则有另外的文档语义与策略。把应用标签当作已经强制执行的访问控制,可能在标签看起来完好的同时暴露内容,因此需要检查实际读写行为。 快速验证 把脚本保存为 xattrs.py,在临时目录支持用户扩展属性的一次性 Linux 容器中,以 root 运行 python3 xattrs.py。实测使用 Linux 6.12.76、Python 3.11.2 和 ext4 卷。root 准备样本,再通过一个普通数字身份启动子进程,不会创建账号: from pathlib import Path from tempfile import TemporaryDirectory import errno,os,shutil,subprocess,sys def fails(code,operation): try:operation() except OSError as error:assert error.errno==code,error else:raise AssertionError('expected attribute error') with TemporaryDirectory(prefix='xbcatm-xattr-') as directory: root=Path(directory);root.chmod(0o755);path=root/'data';path.write_bytes(b'public fixture');path.chmod(0o644) os.setxattr(path,'user.review',b'binary\0value\xff',os.XATTR_CREATE) assert os.getxattr(path,'user.review')==b'binary\0value\xff' fails(errno.EEXIST,lambda:os.setxattr(path,'user.review',b'other',os.XATTR_CREATE)) fails(errno.ENODATA,lambda:os.setxattr(path,'user.missing',b'value',os.XATTR_REPLACE)) os.setxattr(path,'user.empty',b'');assert os.getxattr(path,'user.empty')==b'' os.removexattr(path,'user.empty');fails(errno.ENODATA,lambda:os.getxattr(path,'user.empty')) print('attribute values are bytes; empty differs from missing; CREATE and REPLACE enforce their preconditions') alias=root/'alias';os.link(path,alias) os.setxattr(alias,'user.review',b'shared',os.XATTR_REPLACE) assert os.getxattr(path,'user.review')==b'shared' moved=root/'moved';path.rename(moved);assert os.getxattr(moved,'user.review')==b'shared' plain=root/'plain';preserved=root/'preserved';shutil.copyfile(moved,plain);shutil.copy2(moved,preserved) fails(errno.ENODATA,lambda:os.getxattr(plain,'user.review')) assert os.getxattr(preserved,'user.review')==b'shared' assert plain.read_bytes()==preserved.read_bytes()==moved.read_bytes() print('hard links and same-filesystem rename retain inode attributes; copyfile omits them, copy2 preserves this supported user attribute') os.setxattr(moved,'user.classification',b'private') probe=r''' from pathlib import Path import errno,os,sys path=Path(sys.argv[1]);assert path.read_bytes()==b'public fixture' assert os.getxattr(path,'user.classification')==b'private' try:os.setxattr(path,'user.classification',b'changed') except OSError as error:assert error.errno==errno.EACCES else:raise AssertionError('another identity changed a read-only user attribute') ''' result=subprocess.run([sys.executable,'-c',probe,str(moved)],user=61001,group=61001,extra_groups=[],capture_output=True,text=True,timeout=5) assert result.returncode==0,result.stderr fails(errno.E2BIG,lambda:os.setxattr(moved,'user.too_large',b'x'*65537)) print('a user.classification label does not enforce confidentiality; file permissions block the other identity from changing it; oversized value returns E2BIG') 样本保存同时包含空字符和非 UTF-8 字节的值,并取回完全相同的字节。用 XATTR_CREATE 再次创建相同键,得到 EEXIST;用 XATTR_REPLACE 修改缺失键,得到 ENODATA。这些标志明确表达前提,而不是把所有写入都变成不检查原状态的覆盖。错误结果也是契约的一部分,需要由调用方实际处理。 ...

十二月 14, 2027 · 1 分钟 · XB Field Notes