权限模型:755 之外那几个数字才是坑

权限模型:755 之外那几个数字才是坑
隔壁老康难度 ★★☆☆☆ · 前置:stat 里那十三个字段 | 一句话结论:权限判断是短路的——按 uid、gid、other 顺序命中即停;目录上的
x决定能不能进、r决定能不能列;SUID/SGID/sticky 三个特殊位管的是「以谁的身份」而不是「能不能」。
入职第三个月,我照着内部 wiki 给团队开了个共享目录,四条命令抄得很快,一条都没多想,groupadd 建组,mkdir 建目录,chown 把目录交给这个组,chmod 2775 里那个 2 我甚至没去查它是什么,文档上写着 SGID 能让新建文件继承目录属组,读完这一句我就往下翻别的参数了,因为再往下还有十来个类似的设置等着抄,谁会在意一个数字具体怎么起作用,反正目录建出来能用,同事能往里写东西,任务就算完成了,那年我连 st_uid 和 st_gid 分别在哪一位都没对上过,却已经在给团队配共享目录了,现在想想这个顺序挺离谱的。
抄的就是这四条。
1 | sudo groupadd -f dev # -f 让重复执行不报错 |
那个 2 是 SGID,作用是让目录里新建的文件自动继承目录的属组,没有它的话,每个组员建的文件属组都是自己的主组,别人压根改不了,这一点我当时是理解的,我甚至拿它给同事解释过一遍,讲得头头是道,讲完就以为自己懂了,我还记得自己当时的推理链是,目录属组是 dev,新建文件继承 dev,dev 组的人就有权限,三步都成立,所以这事没有问题。(现在回头看,能背出一句话,和真知道它挡住的是什么,中间差着好几层,而差的这几层,刚好就是后面那个删不掉的 bug 藏身的地方)
当时我觉得这事办得挺漂亮。
两天后同事来找我,说文件都能建,但删不掉别人的,语气还算客气,不过我听得出来他已经在心里给我记了一笔。
我先去看目录本身,ls -ld /srv/share 打出来是 drwxrwsr-x 5 root dev,s 好好地挂在那儿,2775 确实生效了。那就不是 SGID 的问题,我盯着那行输出想了很久,把 SGID 的语义在心里过了一遍又一遍,还是对不上,因为 s 就是在,权限也确实给到了组,组员也确实在里面,直到我换了个方向想,能不能删一个文件,压根不取决于这个文件自己的权限,取决于它所在目录的 w 位,目录可写,里面谁的文件你都能删,包括 root 建的,包括那个文件自己是 444 的。
这条规则只有一句。
但它背后还连着另外两件事,sticky 位,和目录那个 x 到底是什么,我后来才发现整个权限模型最难理解的地方都堆在这三件事上,而 wiki 上用三行就把它们带过去了,那三行我还照着抄过两遍,抄的时候一个字都没进脑子。这两件事在真实环境里出现的频率比想象中高得多,sticky 位每天都在 /tmp 上工作,而那个 x,你只要碰到过一次打不开的目录,就已经被它坑过了,只是当时不知道问题出在哪儿。(顺便说一句,我到今天看见 drwxrwsr-x 这种串还是要在心里数格子,数到第几位是属组,谁也没法一眼扫过去)
这篇把这些一次性说清。
判断顺序是短路的
Linux 的权限模型按 uid/gid 匹配,不是求并集。
我当初以为这三组权限是叠加的,属主那三位不够的时候还能用属组那三位补一点,或者干脆把三组能给的加起来,取一个最宽松的结论,毕竟名字摆在那儿,看着就像三份独立的授权,总有办法凑出想要的结果,这个错误理解我抱着过了大半年,期间每次遇到权限问题我都按这个思路排查,把三组位挨个看一遍,挑一个看起来最宽松的结论,有时候居然也能蒙对,因为多数情况下三组里确实有一组给了权限,蒙对的时候就更不会去怀疑模型本身了。
跑了一遍发现完全不是。
每个文件三组权限位,对应三个身份,
1 | -rwxr-xr-- 1 alice dev 1234 Nov 13 20:40 app.sh |
内核判断的时候是顺序短路,先看你是不是属主,是就只用属主那三位,命中也好拒绝也好,到此结束,后面两组根本不看,不是属主,再看你的任一 gid 是不是等于文件的 st_gid,是就只用属组那三位,都不是,才落到 other,这三个判断是嵌套的,不是并列的,你从哪一档进去就只认哪一档,所以属主位上写着 w 这件事,对一个不是属主的人来说,连参考价值都没有,我第一次知道这个顺序的时候还专门写了个小脚本去撞它,想试试能不能让一个既是属主又在组里的用户同时用上两组权限,撞了一下午才承认这条路根本不存在。(那次实验的结论后来被我写进了一封给新人的邮件里,标题就叫权限不是并集)
graph TD
A["进程发起访问<br/>uid=? gid=?"] --> B{"uid == 文件 st_uid ?"}
B -->|"是"| C["用属主那三位判断<br/>命中或拒绝就结束"]
B -->|"否"| D{"进程的任一 gid<br/>== 文件 st_gid ?"}
D -->|"是"| E["用属组那三位判断<br/>命中或拒绝就结束"]
D -->|"否"| F["用 other 那三位判断"]
C --> G["决定"]
E --> G
F --> G
短路这个词是重点,因为它解释了绝大多数明明给了权限、对方还是说没权限的误会,你给的确实是某三位,而对方根本走不到那三位。
假设文件是 -rw-r----- alice dev,你在 dev 组里但不是 alice,你的 uid 不是 alice,属主那三位 rw- 跳过,你在 dev 组,用属组那三位 r--,结论是能读、不能写。
属主有 w 这件事跟你毫无关系。
这就是最常听到的那句抱怨的来源,明明给了写权限怎么还说没权限,答案是给了,给错了那三位,我第一次把这个判断链条讲清楚的时候自己也愣了一下,因为我一直把它当成一张表在查,左边一列右边一列对上去,从来没意识到它是一条单行道,进去就出来了。
后来带新人我发现,几乎每个人第一次都会问同一个问题,那如果我在组里,组的权限比属主的大,是不是就能用组那一档,答案是不能,因为轮不到,这个问题我至少回答过五遍,每回答一次就更确认这条短路规则有多反直觉。
目录上的 x 不是执行
这是新手最大的坎,也是我卡得最久的地方。
先把三个位在文件上和目录上的区别摆出来,
| 位 | 在普通文件上 | 在目录上 |
|---|---|---|
r |
能读内容 | 能列出里面有哪些名字(ls) |
w |
能改内容 | 能增删改里面的条目 |
x |
能执行 | 能穿过去(cd、访问里面的文件) |
目录的 x 在源码里叫 search 位,不叫执行位,它的意思只有一个,允许这个目录名出现在路径中间,就这么简单,可这句话我第一次读到的时候完全没读懂,因为我脑子里 x 就等同于运行,怎么也套不到一个目录上,我甚至怀疑过是不是 man 手册写错了,直到拿一个 --x 的目录亲手试了一次,才把这两个字对上号。(search 这个叫法是我翻内核源码里 MAY_EXEC 那道检查才注意到的,翻之前我一直以为那是历史遗留命名)
于是这几种组合都构造得出来,--x 那一行最反直觉。
| 目录权限 | ls dir |
cd dir |
cat dir/file |
|---|---|---|---|
r-- |
能列出名字 | 不能进 | 不能读 |
--x |
不能列出 | 能进 | 能读(只要知道文件名) |
rwx |
能列出 | 能进 | 能读 |
--x 是能进不能看,你知道 /etc/passwd 在哪就能 cat 它,但 ls /etc 会被拒,这种目录被人用来藏东西,文件放进去,只把路径给你,不给你列目录的能力,我第一次遇到是在一个老系统的 /var/lib/private 上,当时还以为文件系统坏了。
反过来的坑更常见,只给了 r 没给 x,ls dir 会列出一串名字,但每个名字后面都是 ?,因为 ls 拿到名字之后还要 stat 一下,而 stat 要穿过目录,需要 x,我第一次见的时候盯着那排问号看了半天,第一反应是文件系统崩了,第二反应是 ls 有 bug。
那三个数字:4、2、1
八进制权限最前面那位是特殊位,装三个标志。
| 数字 | 名字 | 在文件上 | 在目录上 |
|---|---|---|---|
4 |
SUID | 执行时以文件属主身份运行 | 无效果 |
2 |
SGID | 执行时以文件属组身份运行 | 新建文件/子目录自动继承目录的属组 |
1 |
sticky | 无效果 | 只有文件属主、目录属主、root 能删里面的文件 |
它们会顶掉属主或其他的 x 位显示,s 和 S 的区别是这里唯一的记忆点。这几个位在 st_mode 里具体落在哪三位,讲 stat 那篇已经一位一位排过了,这里只管它们各自管什么。
| 显示 | 含义 |
|---|---|
s 在属主位(rwsr-xr-x) |
SUID 且可执行(4755) |
S 在属主位(rwSr-xr-x) |
SUID 但不可执行(4644),没用 |
s 在属组位(rwxr-sr-x) |
SGID 且可执行(2755) |
t 在其他位(rwxrwxrwt) |
sticky 且可写(1777) |
大写的 S 和 T 是个警告,特殊位设了,但底下那个 x 没设,这个位不起任何作用,ls -l 特意用大小写区分,就是为了让你一眼看出设了但白设,我头一年完全没注意到这个细节,直到有次给一个脚本 chmod 4644,s 变成了 S,脚本照样跑不起来,我才回去查了这两颗字母到底差在哪。
(顺带说,chmod 4644 这种写法本身也值得警惕,它把特殊位和普通权限压在一个数字里,一旦敲错一位,症状会以完全不相干的形式出现)
SUID 是这一块里最危险的东西,passwd 和 sudo 是绕不开的两个例子,
1 | ls -l /usr/bin/passwd /usr/bin/sudo |
1 | -rwsr-xr-x 1 root root 59976 Feb 12 2024 /usr/bin/passwd |
这两个程序带 s,普通用户跑它们时,进程的 euid 变成 root,这就是为什么你能改自己的密码(要写 /etc/shadow),也能切到 root(要读 /etc/sudoers),一个带 SUID 的 root 程序等于把 root 的钥匙复制了一份发出去,所以它内部任何一处缓冲区溢出都不只是程序崩溃那么简单,那是直通。
euid 才是被检查的那个
内核判断权限,看的不是你是谁,是你此刻以谁的身份在做事。
这句话我入职第二年才真正吃透。
进程身上有三个 uid,分别是 ruid、euid、suid,依次是你真实是谁、内核拿谁判断权限、以及出事之后能切回去用谁。
| 字段 | 意思 |
|---|---|
ruid |
真实 uid,你是谁 |
euid |
有效 uid,内核拿它判断权限 |
suid |
保存的 uid,切回去用 |
内核只看 euid,所以 sudo 能生效,也所以 SUID 程序里任何一个漏洞都等于 root,euid 这个字段是整个权限模型的执行点,前面那三组权限位,比对的其实是 euid 和 egid,不是你的登录身份。
近些年发行版在系统性地收窄 SUID 的范围,ping 从 SUID 改成了 capability(Ubuntu 从 19.04 起装包时就走 setcap,更早的版本里那段逻辑是写死的关闭状态,只 chmod u+s),mtr、traceroute 也一样,整个方向是不再给整把钥匙,只给需要的那一项。
1 | getcap /usr/bin/ping |
1 | /usr/bin/ping cap_net_raw=ep |
s 位没有了,靠 cap_net_raw 授权,这个方向更好,只给需要的那一项能力,而不是整个 root,我拿这个跟同事解释 capability 的时候用过一句话,权限不是减法,是加法,默认什么都不给,用一点开一点。
新建文件为什么是 644
新建文件的权限不是凭空来的,规则是一个按位运算。
1 | 新文件权限 = 请求的权限 & ~umask |
普通 open 默认请求 0666,mkdir 默认请求 0777,然后被 umask 砍掉,所以你敲一次 umask 看到的是什么,之后新建的东西就按什么算,两个默认值之间的那点差,就是目录多出来的 x。
1 | umask |
1 | 0022 |
所以新文件是 0666 & ~0022 = 0644,新目录是 0777 & ~0022 = 0755。
那个 ~ 是位取反,不是减法,有人按 666 - 022 = 644 记,在 umask 007 上就算错了,666 & ~007 = 660,而不是 666 - 007 = 657,这两个数只差三位,可那三位刚好是其他人的读写权限,一个本该只给自己看的文件,就这么变成了全员可读。(这个错我是在 code review 里抓到的,写代码的人推导在中段完全对得上,所以他特别自信,我也差点被说服)
还有一条边界,只有 open(..., mode) 里显式带了 O_CREAT,mode 才参与计算,open 一个已存在的文件时,mode 直接被忽略,所以你不能靠 open 的第三个参数去改一个老文件的权限,那个参数在那一刻就是死的。
umask 是进程自己的属性,fork 会继承,所以 shell 里敲一次 umask 077,之后跑的所有命令都跟着变,这也是那句抱怨的来源,明明 chmod 644 了,程序建出来的文件还是 600,因为程序自己调了 umask()。
ACL 和它那个 mask
先补一句 mask 是什么,它是 ACL 的权限上限,任何一条 ACL 项最终生效的权限是它自己和 mask 的交集,所以有时候你 setfacl 加了人、那人还是读不了,就是被 mask 卡住了。回到正题,三组权限有个硬限制,一个文件只能有一个属主、一个属组。
可现实需求经常是 alice 和 bob 能读写,carol 只读,其他人都别动,用三组权限得先建个组,然后每次加人改组,而组是全局的,会影响这个文件之外的所有东西,一个为了三个人的共享目录去动全系统的组,怎么算都不划算。
ACL 就是为这个场景来的,report.txt 得先存在,setfacl 一般也要 root。
1 | sudo touch /srv/share/report.txt |
1 | # file: srv/share/report.txt |
多了具名条目,也多了一个 mask::。
mask 是 ACL 最坑的一处,它是除属主和 other 之外所有条目的权限上限,你 setfacl -m u:alice:rw,如果 mask 是 r--,alice 实际只有 r,而 getfacl 的输出里 alice 那一行明明白白写着 rw-,看起来完全没问题,我第一次踩这个坑就是因为只看了那一行,还跟同事说配好了。(后来才知道 setfacl 默认会重算 mask,只有在加了 -n 或者用 m:: 显式写的时候,才会出现这种写了却不管用的情况。这个坑我后来又踩过一次,那次是在脚本里批量 setfacl,脚本跑完一件事都没报错,权限却一条都没生效,查了半天才想起 -n)
有 ACL 的文件,ls -l 的权限串末尾会多一个 +,反过来说也成立,没有 + 就一定没有 ACL,所以看到 + 必须 getfacl 看一眼,光读那九位会得出错误结论。
1 | ls -l /srv/share/report.txt |
1 | -rw-rw-r--+ 1 root dev 0 Nov 13 20:52 /srv/share/report.txt |
改 mask 直接写 m:: 那一项,
1 | sudo setfacl -m m::rw /srv/share/report.txt |
setfacl 在写入新条目时会自动重算 mask,除非你加了 -n。
ACL 还有一条被忽略得太多的规则,目录上的 ACL 不会自动继承,想让新建的东西带上,得设默认 ACL,也就是加一个 -d。
1 | sudo setfacl -d -m u:alice:rw /srv/share |
-d 是 default,没有它,ACL 只对当前这一层生效,子文件一个都不带,我碰到过一个共享目录手工配了二十多条 ACL,全是因为没人知道有这个开关,人一多,那个目录就变成了每次进人改一遍的体力活。
让短路真的短一次
短路这件事能直接验,需要一个能切用户的环境,没有的话用 sudo -u 凑合,这几个实验都建议在 /tmp 下做,免得把真实目录搞乱。
1 | sudo mkdir -p /tmp/permtest |
1 | -rw-r----- 1 root root 0 Nov 13 20:53 /tmp/permtest/f.txt |
换个非 root 用户读一遍。
1 | sudo -u nobody cat /tmp/permtest/f.txt |
Permission denied。nobody 不在 root 组,落到 other 那一组(---),没有 r,这件事本身没什么花头,但它说明了一个容易被跳过的事实,权限判断里那个 other 不是你写的那个兜底,它就是大多数情况下真正生效的那一条。
再看目录 x 的威力。
1 | mkdir -p /tmp/xonly && echo secret > /tmp/xonly/data.txt |
第一条被拒,第二条成功,因为列目录要 r,穿目录要 x,711 只给了后者,这两行输出值得放在一起看,同样的目录,同样的人,一个进不去一个进得去,区别只在问的是哪一件事。
顺带说一句,这个实验一定要换用户做,chmod 711 之后你作为属主,属主那三位是 rwx,自己试是看不出区别的,我第一次做这个实验就白做了半小时,还在那儿怀疑是文件系统缓存。
验证 umask 只要两行。
1 | umask 077 |
第一条建出来是 -rw-------,第二条是 -rw-r--r--。
最后看一眼系统上有哪些 SUID 文件,这个清单值得隔段时间过一遍,
1 | find / -perm -4000 -type f 2>/dev/null | head -20 |
-perm -4000 前面那个减号表示至少包含这几位,不是等于,写成 -perm 4000 只会匹配权限恰好等于 4000 的文件,结果基本是空的,这个错误很常见,我在两个不同仓库的运维脚本里都见过它。
权限位上最容易搞反的两处
删除文件需要文件的 w 位吗,需要的是目录的 w,所以共享目录里能删别人的文件是正常行为,要禁掉得靠 sticky 位(1777),或者别的机制,这一条是全文的起点,也是最多人误会的一条。
目录 644 和 755 差不多吗,差很远,目录没有 x,里面的文件就是打不开,哪怕文件本身是 777,因为路径里每一级目录都要穿过去,少一个 x 就断在那一级。
S 和 s 一样吗,s 是特殊位加可执行,S 是特殊位加不可执行,后者无效。
SUID 只在 root 的文件上有意义吗,任何属主都有效,只是非 root 属主的收益小,历史上出现过 SUID 到普通用户被用来提权的漏洞,两次提权串起来就能摸到 root。
setfacl 之后就生效了吗,还要看 mask,+ 一出现就必须 getfacl,因为肉眼能看到的九位权限在这个文件上已经不完整了。
ACL 会自动继承吗,只有目录上设了 -d 才会传给新建的文件。
root 什么都能干吗,root 能绕过所有 DAC 检查,但 SELinux/AppArmor 这类 MAC 层照样限制它,ls -Z 能看到那一层标签,容器里更明显,容器里的 root 在宿主机上只是个普通 uid。
三个数字和一条短路
判定顺序是属主 → 属组 → 其他,命中即停,后面的不看,这一条想通了,后面所有关于权限的困惑基本都能自己推出来,包括为什么给整组加了权限还是有人进不去,为什么属主自己明明 rwx 却打不开文件。
目录的 x 是能不能穿过去,r 是能不能列出来,w 是能不能增删条目,三者独立。
删文件看目录的 w。防这一手用 sticky 位。
4、2、1 三个特殊位,SUID 要 root 权限,SGID 让目录继承属组,sticky 限制删除。
umask 是位与取反,不是减法。
ACL 的 mask 是上限,+ 号出现就必须 getfacl 确认。
到此为止,一个文件在系统里的身份,它叫几个名字、归谁、能干什么,都讲完了,接下来换一个角度,一个进程怎么被创建出来,fork 那句复制进程到底复制了什么,那是另一个我卡过很久的地方。











