0%

为什么Linux删除文件需要文件夹的可执行权限

概述

在Linux系统中,需要对文件所在目录有读写权限和可执行权限,才可以删除文件。读写权限很好理解,因为需要读取和修改文件夹内容信息。而可执行权限感觉有点奇怪,谷歌+分析内核源码,得到答案。

测试

注意:需要使用普通用户进行以下操作

首先使用mkdir创建一个目录,查看其权限,为drwxr-xr-x(755)
然后在该目录下创建文件,查看权限为-rw-r--r--(644)
可以看到文件夹和文件的默认权限是不同的,文件夹具有可执行权限

使用chmod命令设置文件夹权限为600,使用ls命令列出文件,或者rm直接删除文件,都提示权限不足。(要删除的话首先要列出文件,合理)

使用chmod命令设置文件夹权限为100,使用cd命令(通过chdir这一系统调用),正常打开该目录,但无法列出文件。

由此可知,chdir仅检测可执行权限,不检测读权限。而列出文件则需要读和可执行权限。

r:列出文件夹下的文件名(其他信息则需要x)
x:切换该目录为工作目录

探究

为什么需要可执行权限

Unix File and Directory Permissions and Modes

Because directories are not used in the same way as regular files, the permissions work slightly (but only slightly) differently. An attempt to list the files in a directory requires read permission for the directory, but not on the files within. An attempt to add a file to a directory, delete a file from a directory, or to rename a file, all require write permission for the directory, but (perhaps surprisingly) not for the files within. Execute permission doesn’t apply to directories (a directory can’t also be a program). But that permission bit is reused for directories for other purposes.

Execute permission is needed on a directory to be able to cd into it (that is, to make some directory your current working directory).

Execute is needed on a directory to access the inode information of the files within. You need this to search a directory to read the inodes of the files within. For this reason the execute permission on a directory is often called search permission instead.

You can think of read and execute on directories this way: directories are data files that hold two pieces of information for each file within, the file’s name and it’s inode number. Read permission is needed to access the names of files in a directory. Execute (a.k.a. search) permission is needed to access the inodes of files in a directory, if you already know the file’s name.

文件夹的可执行权限被用于chdir,也用于访问目录内文件的inode信息(因此对于文件夹而言,可执行权限通常被称为搜索权限)

源码分析

rm源码

rmGNU coreutils里的一个命令,源码见:
remove.c
最终调用excise函数删除文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* Remove the file system object specified by ENT.  IS_DIR specifies
whether it is expected to be a directory or non-directory.
Return RM_OK upon success, else RM_ERROR. */
static enum RM_status
excise (FTS *fts, FTSENT *ent, struct rm_options const *x, bool is_dir)
{
int flag = is_dir ? AT_REMOVEDIR : 0;
if (unlinkat (fts->fts_cwd_fd, ent->fts_accpath, flag) == 0)
{
if (x->verbose)
{
printf ((is_dir
? _("removed directory %s\n")
: _("removed %s\n")), quoteaf (ent->fts_path));
}
return RM_OK;
}

/* The unlinkat from kernels like linux-2.6.32 reports EROFS even for
nonexistent files. When the file is indeed missing, map that to ENOENT,
so that rm -f ignores it, as required. Even without -f, this is useful
because it makes rm print the more precise diagnostic. */
if (errno == EROFS)
{
struct stat st;
if ( ! (lstatat (fts->fts_cwd_fd, ent->fts_accpath, &st)
&& errno == ENOENT))
errno = EROFS;
}

if (ignorable_missing (x, errno))
return RM_OK;

/* When failing to rmdir an unreadable directory, we see errno values
like EISDIR or ENOTDIR (or, on Solaris 10, EEXIST), but they would be
meaningless in a diagnostic. When that happens and the errno value
from the failed open is EPERM or EACCES, use the earlier, more
descriptive errno value. */
if (ent->fts_info == FTS_DNR
&& (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR
|| errno == EEXIST)
&& (ent->fts_errno == EPERM || ent->fts_errno == EACCES))
errno = ent->fts_errno;
error (0, errno, _("cannot remove %s"), quoteaf (ent->fts_path));
mark_ancestor_dirs (ent);
return RM_ERROR;
}

可以看到是通过unlinkat系统调用删除文件(也可使用strace跟踪系统调用)

内核源码

fs/namei.c

其中定义了unlinkatunlink系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SYSCALL_DEFINE3(unlinkat, int, dfd, const char __user *, pathname, int, flag)
{
if ((flag & ~AT_REMOVEDIR) != 0)
return -EINVAL;

if (flag & AT_REMOVEDIR)
return do_rmdir(dfd, getname(pathname));
return do_unlinkat(dfd, getname(pathname));
}

SYSCALL_DEFINE1(unlink, const char __user *, pathname)
{
return do_unlinkat(AT_FDCWD, getname(pathname));
}

删除文件时,是调用do_unlinkat函数,跟踪内部第一个函数filename_parentat的调用链:path_parentat-link_path_walk-may_lookup-inode_permission

调用inode_permission时传入了MAY_EXEC,如下:

1
2
3
4
5
6
7
8
9
10
static inline int may_lookup(struct user_namespace *mnt_userns,
struct nameidata *nd)
{
if (nd->flags & LOOKUP_RCU) {
int err = inode_permission(mnt_userns, nd->inode, MAY_EXEC|MAY_NOT_BLOCK);
if (err != -ECHILD || !try_to_unlazy(nd))
return err;
}
return inode_permission(mnt_userns, nd->inode, MAY_EXEC);
}

其中MAY_EXEC定义于include/linux/fs.h

1
2
3
4
5
6
7
8
9
#define MAY_EXEC		0x00000001
#define MAY_WRITE 0x00000002
#define MAY_READ 0x00000004
#define MAY_APPEND 0x00000008
#define MAY_ACCESS 0x00000010
#define MAY_OPEN 0x00000020
#define MAY_CHDIR 0x00000040
/* called from RCU mode, don't block */
#define MAY_NOT_BLOCK 0x00000080

可以看到是通过调用inode_permission函数,对可执行权限进行判断,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int inode_permission(struct user_namespace *mnt_userns,
struct inode *inode, int mask)
{
int retval;

retval = sb_permission(inode->i_sb, inode, mask);
if (retval)
return retval;

if (unlikely(mask & MAY_WRITE)) {
/*
* Nobody gets write access to an immutable file.
*/
if (IS_IMMUTABLE(inode))
return -EPERM;

/*
* Updating mtime will likely cause i_uid and i_gid to be
* written back improperly if their true value is unknown
* to the vfs.
*/
if (HAS_UNMAPPED_ID(mnt_userns, inode))
return -EACCES;
}

retval = do_inode_permission(mnt_userns, inode, mask);
if (retval)
return retval;

retval = devcgroup_inode_permission(inode, mask);
if (retval)
return retval;

return security_inode_permission(inode, mask);
}
EXPORT_SYMBOL(inode_permission);

查看do_inode_permission

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline int do_inode_permission(struct user_namespace *mnt_userns,
struct inode *inode, int mask)
{
if (unlikely(!(inode->i_opflags & IOP_FASTPERM))) {
if (likely(inode->i_op->permission))
return inode->i_op->permission(mnt_userns, inode, mask);

/* This gets set once for the inode lifetime */
spin_lock(&inode->i_lock);
inode->i_opflags |= IOP_FASTPERM;
spin_unlock(&inode->i_lock);
}
return generic_permission(mnt_userns, inode, mask);
}

最终调用generic_permission

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int generic_permission(struct user_namespace *mnt_userns, struct inode *inode,
int mask)
{
int ret;

/*
* Do the basic permission checks.
*/
ret = acl_permission_check(mnt_userns, inode, mask);
if (ret != -EACCES)
return ret;

if (S_ISDIR(inode->i_mode)) {
/* DACs are overridable for directories */
if (!(mask & MAY_WRITE))
if (capable_wrt_inode_uidgid(mnt_userns, inode,
CAP_DAC_READ_SEARCH))
return 0;
if (capable_wrt_inode_uidgid(mnt_userns, inode,
CAP_DAC_OVERRIDE))
return 0;
return -EACCES;
}

/*
* Searching includes executable on directories, else just read.
*/
mask &= MAY_READ | MAY_WRITE | MAY_EXEC;
if (mask == MAY_READ)
if (capable_wrt_inode_uidgid(mnt_userns, inode,
CAP_DAC_READ_SEARCH))
return 0;
/*
* Read/write DACs are always overridable.
* Executable DACs are overridable when there is
* at least one exec bit set.
*/
if (!(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO))
if (capable_wrt_inode_uidgid(mnt_userns, inode,
CAP_DAC_OVERRIDE))
return 0;

return -EACCES;
}
EXPORT_SYMBOL(generic_permission);

看到注释Searching includes executable on directories, else just read.

(具体是不是这里检测,并不是非常确定,还有待进一步调试内核)

PS:capable_wrt_inode_uidgid函数用于判断用户是否具有忽略DAC访问限制的能力,详见[转载] Linux的capability深入分析

CAP_DAC_OVERRIDE 1 忽略对文件的所有DAC访问限制
CAP_DAC_READ_SEARCH 2 忽略所有对读、搜索操作的限制

参考

Difference between executable directory vs executable files
What permissions are needed to delete a file in unix?
Where can i get the source code of rm command
Linux删除文件过程解析
[转载] Linux的capability深入分析