进程的 umask 在普通模式位创建规则下,为新文件和目录移除权限位。应用先提供请求模式,掩码再限制这份请求。它既不是完整访问策略,也不会重写所有既有文件。如果父目录具有默认访问控制列表,创建计算方式还会改变,因此只检查进程掩码,可能无法准确预测最后得到的权限。排查时需要同时看到应用请求和目录环境。
坏掉的样子
没有默认 ACL 时,普通权限结果是请求模式与 umask 按位取反后的值进行按位与。例如请求 0666、掩码 0022,得到 0644。这个计算不是算术减法:相同掩码下请求 0600,仍然得到 0600。应用没有请求执行权限时,宽松掩码也不会主动添加执行位;掩码负责移除,而不是凭空授予应用未请求的普通权限。
限制严格的 umask 不会修复已经存在的全员可写文件。打开并截断该文件,会改变内容,却保留既有模式。后续 chmod 也可以不受当前掩码限制地扩大权限。因此,把 umask 当作权限永远不能超过的上界,会遗漏既有文件与之后权限修改两类情况。必须区分文件首次创建,以及此后围绕同一 inode 发生的操作。
父目录具有默认 ACL 时,Linux 从它派生新对象的访问 ACL,再按应用请求模式限制权限,不再按普通方式应用进程 umask。因此,即使创建者掩码是 0077,共享目录里仍可能生成组可访问文件。遇到与预期不同的模式时,应检查继承的 ACL,而不能只看到严格掩码就断言内核没有遵守权限设置。
快速验证
把脚本保存为 mask.py,在安装 Python 3 和 setfacl 的 Linux 环境中运行 python3 mask.py。临时目录所在文件系统必须支持默认 ACL。实测使用 Python 3.11.2、acl 2.3.1,以及通过 TMPDIR 选择的独立 ext4 卷。最初 tmpfs 环境拒绝设置默认 ACL,因此完整实验随后在 ext4 上重新执行:
from pathlib import Path
from tempfile import TemporaryDirectory
import os,stat,subprocess
def create(path,requested):
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,requested);os.close(fd)
def mode(path):return stat.S_IMODE(path.stat().st_mode)
previous=os.umask(0o022)
try:
with TemporaryDirectory(prefix='xbcatm-umask-') as directory:
root=Path(directory)
data=root/'data';create(data,0o666);assert mode(data)==0o644
executable=root/'executable';create(executable,0o777);assert mode(executable)==0o755
private=root/'private';create(private,0o600);assert mode(private)==0o600
folder=root/'folder';folder.mkdir(mode=0o777);assert mode(folder)==0o755
assert 0o600&~0o022==0o600 and 0o600-0o022!=0o600
print('without default ACL: requested mode AND NOT umask; 0666 becomes 0644, 0777 becomes 0755')
os.umask(0o077)
strict=root/'strict';create(strict,0o666);assert mode(strict)==0o600
existing=root/'existing';create(existing,0o666);existing.chmod(0o666)
fd=os.open(existing,os.O_WRONLY|os.O_TRUNC);os.close(fd)
assert mode(existing)==0o666
print('restrictive mask leaves an existing 0666 file unchanged; chmod can widen permissions later')
acl_folder=root/'acl';acl_folder.mkdir()
subprocess.run(['setfacl','-d','-m','u::rwx,g::rwx,o::---',str(acl_folder)],check=True)
inherited=acl_folder/'inherited';create(inherited,0o666)
restricted=acl_folder/'restricted';create(restricted,0o600)
assert mode(inherited)==0o660 and mode(restricted)==0o600
print('with default ACL and umask 0077: request 0666 yields 0660; request 0600 still yields 0600')
status=dict(line.split(':',1) for line in Path('/proc/self/status').read_text().splitlines() if ':' in line)
assert int(status['Umask'].strip(),8)==0o077
print('/proc/self/status observes this process mask without temporarily changing it')
finally:os.umask(previous)
掩码为 0022、没有继承默认 ACL 时,请求 0666 得到 0644 文件;请求 0777,普通文件和目录均得到 0755;请求 0600 则仍为 0600。脚本还检查算术减法会对最后一种请求产生不同结果,直接暴露常见计算错误。这些结果说明,需要理解位操作,而不是把八进制权限数字当作普通额度相减。
改为掩码 0077 后,新文件请求 0666,最终得到 0600。另一个文件被明确 chmod 为 0666,随后以截断方式打开,但没有创建操作,其模式保持 0666。这个对照区分首次创建与既有 inode 上的后续操作,而不是假定每次 open 都重新套用创建策略。严格掩码无法替代对存量权限的检查。
接着,脚本给父目录设置默认 ACL:所属用户和组拥有完整权限,其他用户没有权限。进程掩码仍为 0077,新文件请求 0666,却得到 0660。继承 ACL 在严格掩码下保留组读写权限,同时应用请求排除了执行权限。结果来自实际文件状态检查,不是只看 setfacl 命令执行成功就推测新文件行为。
同一目录中另一个文件请求 0600,结果仍为 0600。默认规则不会添加请求模式排除的权限。这两种请求一起验证了默认 ACL 对普通掩码规则的例外,也验证应用显式请求仍然参与限制。只观察较宽的一种结果,容易误以为 ACL 可以忽略应用的所有权限请求,因此必须保留这个对照。
脚本从 /proc/self/status 读取 Umask,确认进程掩码仍为 0077,在 finally 中恢复原值并清理临时目录。如果文件系统无法设置 ACL,命令会失败,不会跳过核心例外后声称完整通过。前一次环境失败与之后完整执行的结果分别保留,避免把环境能力缺失误写成契约已经验证。
在正确层级应用创建策略
umask 会随正常进程创建继承,并在执行另一个程序后保留。服务或命令应通过真实启动路径设置它。无关终端的掩码不会追溯改变已经运行的进程,shell 配置文件也只有被启动链读取时才生效。需要验证的是最终创建文件的进程状态,而不是另一个方便观察的控制终端。
对于共享相关文件系统状态的线程,umask 是进程范围的设置。为了读取旧值而临时改动它,可能干扰同时发生的文件创建。Linux 上读取 proc 状态字段可以避免这种修改。应用如果必须创建私有文件,应请求足够严格的权限,并核对真实存储环境,不能依赖一次短暂改掩码覆盖所有并发创建行为。
还应一起检查父目录访问权限、所有权、ACL 和最终文件权限。0660 文件位于不可访问父目录中时,组用户并不会自动获得完整访问路径;模式位也不能单独描述所有命名 ACL 条目。本实验验证创建结果,端到端访问则需要让预期身份沿完整路径实际尝试。权限数字正确,只是整条访问条件中的一部分。