Linux文件权限模型的核心并不神秘,它就是一个由三个三元组组成的九位二进制位图。每一个文件或目录在创建时,内核会根据调用open、creat或mkdir时传入的mode参数,结合当前进程的umask值,计算出一个最终的mode_t。这个mode_t被写入文件的inode元数据中,之后每一次访问该文件,内核的权限检查逻辑都会读取inode中的mode字段,与当前进程的凭证做比较。很多人习惯用ls -l看到的rwxr-xr-x来理解权限,但真正影响内核决策的是那一串二进制位,rwx只是这串位图在用户空间的可读表达。数字模式0755对应的二进制是111 101 101,从高位到低位依次是属主、属组、其他用户的读、写、执行位。八进制只是一种紧凑写法,每三位二进制正好映射为一个八进制数,所以0755中的7就是二进制111,代表属主拥有读写执行全权限。

内核在权限检查时并不是简单地把九个位拆开分别判断,而是按照固定的优先级顺序进行匹配。首先判断当前进程的有效用户ID是否等于文件属主的UID,如果相等,只使用属主权限位,后面的属组和其他用户权限位完全被忽略。如果属主不匹配,再检查当前进程的有效组ID或附加组列表中是否包含文件属组的GID,若有,则使用属组权限位。如果前两者都不匹配,最后才落到其他用户权限位上。这个顺序意味着即使属主权限位是000,而属组权限位是777,只要进程是文件的属主,它依然无法读取该文件。很多初学者在搭建共享目录时遇到“属主明明是root,为什么普通用户有时能读有时不能读”的问题,根源就是这种短路式匹配逻辑。
特殊权限位如何改变进程身份与目录语义
除了常规的九个权限位,mode_t中还包含三个特殊位:setuid、setgid和sticky bit。setuid位作用于可执行文件时,内核会在exec系统调用中将进程的有效UID临时切换为文件属主的UID,而真实UID保持不变。这个机制让普通用户可以运行passwd这类需要修改/etc/shadow的程序,因为passwd文件的属主是root且带有setuid位,进程运行期间就拥有了root的有效身份。setuid只对二进制可执行文件生效,对脚本文件无效,这是为了防止通过shebang解释器绕过权限检查。setgid位在文件上的作用类似,但切换到文件属组的GID;在目录上则表现为新建的子文件自动继承该目录的属组,常用于团队协作目录。
sticky bit目前只对目录有意义,典型应用是/tmp目录。一个目录如果设置了sticky bit,那么只有文件属主、目录属主或root才能删除或重命名该目录中的文件,即使其他用户对目录拥有写权限也不行。如果没有sticky bit,任何对目录有写权限的用户都可以删除目录中任意文件,即使该文件不属于自己。这个限制背后的设计意图是防止恶意用户通过删除他人文件来制造拒绝服务。目录的执行位本身也有特殊含义:它代表“搜索权限”,即允许进程进入该目录并访问目录中的文件名。如果只有读权限而没有执行权限,用户可以列出文件名但无法访问文件内容;反过来有执行权限没有读权限,则可以进入目录并直接访问已知路径的文件,但无法通过ls列出目录内容。
umask与默认权限的计算逻辑
umask的值决定了新建文件或目录时哪些权限位会被默认屏蔽掉。它的计算方式不是简单的相减,而是先对umask取反,再与调用open或creat时传入的mode做按位与。例如creat一个新文件时mode通常传入0666,如果umask是0022,那么最终权限是0666 & ~0022,也就是0666 & 0755,结果为0644。目录创建时mode通常传入0777,umask 0022下目录最终权限为0755。很多人误以为umask是从mode中减去对应数字位,比如0666减0022等于0644,这个碰巧对,但如果是umask 0033,0666减去0033等于0633,而实际按位运算结果是0633吗?0666的二进制是110 110 110,0033是000 011 011,取反后是111 100 100,相与得到110 100 100也就是0644。所以相减的结果0633是错误的。根因在于八进制每一位是三个独立位,按位与操作不能跨位借位。
查看和修改当前shell进程的umask可以使用umask命令,不带参数输出当前值,带参数则设置新值。常见的umask值包括022、027和077。022允许组和其他用户拥有读和执行权限,但去掉写权限;027进一步去掉组的写权限,其他用户只有读和执行权限,常用于多用户共享主机;077则完全屏蔽组和其他人的所有权限,只保留属主权限,适合私人目录。umask设置只影响当前进程及其子进程,不会改变已有文件的权限。修改系统全局默认umask通常写在/etc/profile或/etc/login.defs中,不同发行版位置略有差异。在编写程序时,可以通过umask系统调用在进程内动态调整,注意该调用会立即生效且适用于后续所有文件创建操作。
权限检查在系统调用中的具体位置与绕过机制
权限检查并不发生在某个单独的函数中,而是嵌入在VFS层的多个入口点。以open系统调用为例,内核路径为do_sys_open -> do_filp_open -> path_openat,在path_openat中会调用may_open函数,may_open内部通过inode_permission检查inode的权限位。inode_permission根据当前进程的cred结构和inode的i_mode字段,调用generic_permission进行实际判断。如果权限不足,函数返回-EACCES,open最终向用户空间报告Permission denied。对于读、写操作,read和write系统调用则通过file_permission或rw_verify_area再次检查,因为在打开文件后权限可能已经被chmod修改。这种多级检查保证了即使文件描述符已经打开,权限变更依然能立刻生效。
除了传统的DAC模型,Linux还支持POSIX ACL和SELinux、AppArmor等强制访问控制机制。ACL通过扩展属性存储额外的用户和组权限条目,允许为特定用户单独授权而不改变属主或属组。getfacl和setfacl命令可以查看和修改ACL,ls -l中权限位后出现加号表示文件带有ACL。内核在检查权限时,如果发现ACL存在,会优先使用ACL中的命名用户条目,再使用常规的属主、属组条目。SELinux则提供更细粒度的类型强制策略,即使DAC权限允许访问,SELinux策略依然可以禁止。这些扩展机制没有取代传统的rwx模型,而是建立在相同的inode权限检查框架之上,理解基础位图逻辑是排查权限问题的首要步骤。
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
struct stat st;
if (stat("/tmp/testfile", &st) == -1) {
perror("stat");
return 1;
}
// mode_t低12位:特殊位(3位) + 属主(rwx) + 属组(rwx) + 其他(rwx)
unsigned short perm = st.st_mode & 07777;
printf("mode: %o\n", perm);
printf("owner: %o\n", (perm >> 6) & 07);
printf("group: %o\n", (perm >> 3) & 07);
printf("other: %o\n", perm & 07);
printf("setuid: %d, setgid: %d, sticky: %d\n",
(perm & 04000) != 0,
(perm & 02000) != 0,
(perm & 01000) != 0);
return 0;
}
在实际排查权限问题时,除了查看ls -l输出,使用stat命令可以精确看到八进制模式和特殊权限位。如果一个进程在特定目录下无法创建文件,首先检查目录的写和执行权限,然后确认进程的有效gid是否属于目录属组,再检查umask是否屏蔽了所需权限。对于setuid程序,使用id命令可以查看真实和有效UID的差异。权限问题的表现往往是单调的Permission denied,但背后的成因可能涉及umask、ACL、SELinux标签甚至挂载选项中的noexec标志。系统管理员需要建立从系统调用到用户命令的完整链路认知,才能在面对复杂环境时快速定位根因。

