linux版本为 v6.7
以chroot修改根目录为例,走一遍流程,重点在path_lookupat的实现。代码按逻辑组织,非真实代码顺序。由于涉及太多细节,每部分的开始会先做一个小结。
解析路径字符串,逐层进入,检查权限后将current->fs->root改为对应的路径节点,如果逐层进入子路径的过程中遇到ESTALE错误,可能是某个文件子系统的缓存失效了,这时可以尝试标记LOOKUP_REVAL对可以提示子文件系统对缓存项做及时检验,重来一次。
SYSCALL_DEFINE1(chroot, const char __user *, filename):
// 进入到路径节点
user_path_at();
// 检查权限
path_permission(&path, MAY_EXEC | MAY_CHDIR);
ns_capable(current_user_ns(), CAP_SYS_CHROOT);
// selinux 安全模块的 hook
security_path_chroot(&path);
// 将current->fs->root设置为传入路径对应的节点
set_fs_root(current->fs, &path);
// 如果路径有问题,可能是一些文件系统的缓存导致的
// 设置LOOKUP_REVAL可以在需要验证的地方对缓存项做验证
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
先生成一个绑定struct audit_names信息的路径字符串节点 struct filename(getname_flags),再一层层进入节点(path_lookupat),并更新节点inode的最新信息(audit_inode)。
user_path_at():
getname_flags():
// 从缓存读取路径对应的struct filename
audit_reusename();
// 如果没有缓存没有,就取一个预分配的filename对象
// 这里接受的路径最长是4k减去一个struct头的大小
// 如果路径字符串超过了这个值,但小于4k,
// 可以分配一个额外的 struct 头,原对象的4k空间完全用来存字符串
...
// 存入audit_context->names_list缓存
audit_getname();
filename_lookup():
// 创建nameidata结构,它是用户向内核传参数的媒介
set_nameidata();
// 尝试进入路径节点,这里有三种尝试
// 先尝试RCU方式读,如果并发修改频繁,很可能rcu在中间会读到脏数据,导致失败ECHILD
// 再尝试普通读方式,需要加锁,如果访问到一些子文件系统的失效缓存,导致失败ESTALE
// 最后尝试加上LOOKUP_REVAL标记,提醒子文件系统,读到缓存后先做一次校验
path_lookupat();
// 存一份本inode的最新信息
audit_inode();
// 恢复原来的current->nameidata
restore_nameidata();
切换工作目录到指定目录,如果它是mount节点,要先解析切换到mount的目录。然后一层层解析路径并进入,解析中发现符号链接时需要以深度遍历的方式先解析符号链接(符号链接的路径也可能有mount节点)。到最后一层时如果发现是个mount节点,且调用者指定LOOKUP_MOUNTPOINT标记,则需要对最后一层节点做mount操作。
最后检查最后进入的位置是否合法,做一下清理工作,并返回。
path_lookupat():
// 找到工作路径放在nameidata->path上,有三种可能情况:
// 1、根目录:路径以'/'开头
// 2、当前目录: nameidata->dfd == AT_FDCWD
// 3、指定目录:nameidata->dfd 为真实的文件描述符fd,比如LOOKUP_IS_SCOPED的场景
path_init();
// 有LOOKUP_DOWN标记,意味着入口可能是一个mount的节点,要解析一下mount
handle_lookup_down();
step_into():
// 如果工作路径是挂载节点,则一层层进入挂载点(它也可能只是普通目录)
handle_mounts();
// 如果遇到符号链接,最多深度链接38次,为它预先分配40个slot
// 直到整个流程结束,restore_nameidata时才释放
//(原本只有2个slot,存上一级和当前级目录,因为如果是普通目录
// 或mount节点的话,每解析一层,就进入对应目录,只需要2个slot)
pick_link();
// 从工作目录一层层进入到最终节点
link_path_walk();
// 有LOOKUP_MOUNTPOINT标记,说明目标节点也是mount的子系统节点,进入mount节点。
handle_lookup_down();
// 检验路径是否合理,有两方面:
// 1、对于LOOKUP_IS_SCOPED,要判断路径是在root节点之后
//(这只是兜底逻辑,实际前面每次解析节点都已经做过判断)
// 2、如果有DCACHE_OP_WEAK_REVALIDATE标记,还要检验下缓存没过期
complete_walk();
// 如果因为错误导致的link_path_walk提前返回,则栈上的资源需要释放:
// 1、调用所有路径的释放hook,比如有的文件子系统要关闭设备
// 2、将栈中所有 path 的引用计数减1
if (err)
terminate_walk();
解析路径字符串,每一层以一个或多个'/'结束,每一层可能是点,点点,或普通路径实体。调用walk_component向下走(中间可能遇到三种情况:普通路径实体、符号链接、mount节点)。直到最后一层,不再向下走。最后一层可能是普通实体节点或mount节点,不可能是link节点,并且即使是mount节点也不做解析。
link_path_walk();
while true:
// 查看是否有exec权限
idmap = mnt_idmap(nd->path.mnt);
err = may_lookup(idmap, nd);
// 解析下一层的字符串(以‘/’结尾),可能是.(当前目录)..(上层目录),实体目录
hash_len = hash_name(nd->path.dentry, name); // 取一级字符串
...
switch(当前一层路径实体):
case 是普通路径实体:
// 进入下一层
walk_component(WALK_MORE);
case 是个 link:
// 如果下一层是个 link,则放在栈里,下个循环里先解析link的路径
nd->stack[depth++].name = name;
name = link;
case 走到了一个link的最后一个节点:
// link路径走完,则回到上一个栈的位置
name = nd->stack[--depth].name;
walk_component(0);
case 走完了全部节点(*name == nullptr && depth == 0),不再向下走:
return 0;
// 校验它是目录,不是文件
d_can_lookup()
跳过已经mount好的节点后,对本目录和下层目录拿锁,执行一层层mount挂载(如果是链式挂载的话)。这里涉及三个宏和一个函数需要简单讲一下:
1、DCACHE_MANAGE_TRANSIT:从这个文件找子目录需要特别操作,比如等待其它的依赖mount完成等。
2、DCACHE_MOUNTED:mount已经完成,在缓存的mount_hashtable哈希表中会有记录。
3、DCACHE_NEED_AUTOMOUNT:需要在第一次访问时自动挂载。
4、try_to_unlazy_next与try_to_unlazy:mount操作是需要排它执行的,虽然前面读可以用rcu,但要做mount操作的话,还是要对节点拿锁,锁的节点涉及符号链接、根目录、当前进入的目录、mount节点。加next的函数与不加的区别在于,加next的需要锁本层与下层目录,不加只锁本层目录。
handle_mounts():
// 跳过普通目录或已经mount好的节点
__follow_mount_rcu():
// 如果节点无需要auto mount直接返回,需要的话,先验证目录有效,加锁,才进一步mount
// (验证link, mnt,path,root依然有效,则增加锁引用,这是退出rcu模式前要做的)
try_to_unlazy_next();
// 一层层进入挂载点执行mount操作(follow_automount)
traverse_mounts();
/* --------------------------------------- */
// 跳过普通目录或已经mount好的节点
__follow_mount_rcu():
while true:
// 从这个文件找子目录需要特别操作
//(比如等待其它的依赖mount完成,如果rcu访问的话不用等)
if (unlikely(flags & DCACHE_MANAGE_TRANSIT)) {
int res = dentry->d_op->d_manage(path, true);
if (res)
return res == -EISDIR; // 它只是个普通的目录文件
}
// 检查是否已经mounted,如果是,则进入这个目录
if (flags & DCACHE_MOUNTED) {
struct mount *mounted = __lookup_mnt(path->mnt, dentry);
if (mounted) {
path->dentry = dget(mounted->mnt_root);
continue; // 继续查这个被mount的目录是否需要mount
}
}
// 没有mount过,返回是否无需auto mount
return !(flags & DCACHE_NEED_AUTOMOUNT);
/* --------------------------------------- */
// 一层层mount,这里省略了修改mnt计数相关逻辑
__traverse_mounts():
while (flags & DCACHE_MANAGED_DENTRY) {
// 首先调用 manage hook做准备(比如等其它依赖完成)
if (flags & DCACHE_MANAGE_TRANSIT) {
ret = path->dentry->d_op->d_manage(path, false);
if (res)
return res; // 它只是个普通的目录文件
}
// 检查是否已经mounted,如果是,则进入这个目录
if (flags & DCACHE_MOUNTED) {
struct mount *mounted = __lookup_mnt(path->mnt, dentry);
if (mounted) {
path->dentry = dget(mounted->mnt_root);
continue; // 继续查这个被mount的目录是否需要mount
}
}
// 无需 auto mount,可直接返回
// 目录没有被mount,但有可能不需要auto mount,
// 或auto mount 发现是个普通目录,则找到这里就可以返回了
if (!(flags & DCACHE_NEED_AUTOMOUNT) || follow_automount() == -EISDIR) {
return 0;
}
}
默认的栈只有两层,用来存平层与上一层,但link的情况导致可能栈会有多层,举例比如:
用户输入 /a/b/c 目录,解析到b时,栈顶0为空,但发现b是link到了 /a2/b2/c2 目录,于是栈变成了
0:b
走到c2发现它link到了/a3/b3/c3,则栈变成了
0:b,1:c2
如果link没填充过,在填充link时要先拿锁退出rcu。
pick_link返回的是link的路径字符串,如果开始是'/',则要先跳到根节点,再返回后面的字符串。
pick_link
// 默认的栈只有两层,但link的情况导致可能栈会有多层,
// 要额外分配,在回到terminate_walk时才释放栈
reserve_stack():
// 先以rcu模式分配MAXSYMLINKS(40)个栈
nd_alloc_stack();
// 如果不成功,加锁后再重新分配
legitimize_path();
nd_alloc_stack();
// 如果开启sysctl_protected_symlinks,则做检查权限
may_follow_link():
// 三种情况有权限:
// 1、用户是link所有者
// 2、父路径没有设置只有目录所有者和具有写权限的用户可以删除或重命名该目录中的文件
// (S_ISVTX: 只目录所有者,S_IWOTH:只有写权限的用户)
// 3、自己是父目录的所有者
...
// 如果失败,则读审计日志
audit_inode() // 获取inode信息
audit_log_path_denied("follow_link") // 记follow_link错误日志
// 更新访问时间
atime_needs_update
// selinux安全子模块的 hook
security_inode_follow_link();
// 如果有inode->i_link直接用,没有则获取
try_to_unlazy();
inode->i_op->get_link();
// 如果链接字符串是'/',则跳到根节点,并返回link后面的字符串
nd_jump_root();
找到需要审计的规则,删除因为路径上inode节点变化导致的已过时的审计信息chunk,更新最新的路径审计信息。
audit_inode():
// 遍历audit_filter_list[AUDIT_FILTER_FS]中的每个规则,
// 找到审计的inode满足规则涉及的field,如果规则上指示的是AUDIT_NEVER,
// 则无需存这个inode的缓存。
list_for_each_entry_rcu(e, list, list) {
for (i = 0; i < e->rule.field_count; i++)
if (audit_comparator(inode->i_sb->s_magic,f->op, f->val))
return;
}
// 如果已经有审计项,且发生过rename或drop,则后面的项可以清理掉,然后更新这个缓存
handle_path();
audit_copy_inode();
// 如果没有就分配,然后更新这个缓存
audit_alloc_name();
handle_path();
audit_copy_inode();
/* ----------------------- */
// 如果路径上有节点变了,需要更新审计节点。
handle_path():
retry:
// 找到drop的path或rename的path的审计块,做清理(audit_put_chunk)
while true:
// 一层层向上找inode,查看inode是否有审计标记i_fsnotify_marks
if (inode && unlikely(inode->i_fsnotify_marks)) {
// 有的话,说明关心这个inode的审计,找出审计块
chunk = audit_tree_lookup(inode);
if (chunk) {
if (unlikely(!put_tree_ref(context, chunk))) {
audit_put_chunk(drop);
unroll_tree_refs():
for(节点后面的每个chunk) audit_put_chunk();
goto retry;
}
}
parent = d->d_parent;
if (parent == d) {
// 发生了rename
if (read_seqretry(&rename_lock, seq)) {
unroll_tree_refs():
for(节点后面的每个chunk) audit_put_chunk();
goto retry;
}
return; // 正常扫描到了根节点
}
}