Linux下ls命令显示符号链接权限为777的探索
Posted ASCII0x03
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux下ls命令显示符号链接权限为777的探索相关的知识,希望对你有一定的参考价值。
Linux下ls命令显示符号链接权限为777的探索
——深入ls、链接、文件系统与权限
by Ascii0x03,http://www.cnblogs.com/ascii0x03/p/6442420.html
一、摘要
ls是Linux和Unix下最常使用的命令之一,主要用来列举目录下的文件信息,-l参数允许查看当前目录下所有可见文件的详细属性,包括文件属性、所有者、文件大小等信息。但是,当其显示符号链接的属性时,无论其指向文件属性如何,都会显示777,即任何人可读可写可执行。本文从ls命令源码出发,由浅入深地分析该现象的原因,简略探究了Linux 4.10下的符号链接链接、文件系统与权限的源码实现。
关键词:Linux ls 符号链接 文件系统 权限 源码分析
二、引言
2.1 Linux文件权限
在Linux中每个文件有所有者、所在组、其它组的概念[11]。所有者一般为文件的创建者,谁创建了该文件,就天然的成为该文件的所有者;当某个用户创建了一个文件后,这个文件的所在组就是该用户所在的组;除开文件的所有者和所在组的用户外,系统的其它用户都是文件的其它组。ls 命令将每个由 Directory 参数指定的目录或者每个由 File 参数指定的名称写到标准输出,以及所要求的和标志一起的其它信息。ls -l中显示的内容常如下所示:
-rwxrw-r‐-1 root root 1213 Feb 2 09:39 abc |
前10个字符说明了文件类型与权限。第一个字符代表文件(-)、目录(d),链接(l),其余字符每3个一组(rwx),读(r)、写(w)、执行(x)。第一组rwx:文件所有者的权限是读、写和执行;第二组rw-:与文件所有者同一组的用户的权限是读、写但不能执行;第三组r--:不与文件所有者同组的其他用户的权限是读不能写和执行。权限也可用数字表示为:r=4,w=2,x=1 因此rwx=4+2+1=7。
2.2 符号链接
如前所述,若第一个字符显示为l,说明该文件是符号链接。符号链接(软链接)是一类特殊的文件, 其包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用[12]。符号链接的操作是透明的:对符号链接文件进行读写的程序会表现得直接对目标文件进行操作。某些需要特别处理符号链接的程序(如备份程序)可能会识别并直接对其进行操作。一个符号链接文件仅包含有一个文本字符串,其被操作系统解释为一条指向另一个文件或者目录的路径。它是一个独立文件,其存在并不依赖于目标文件。如果删除一个符号链接,它指向的目标文件不受影响。如果目标文件被移动、重命名或者删除,任何指向它的符号链接仍然存在,但是它们将会指向一个不复存在的文件。这种情况被有时被称为被遗弃。
但是,我们常常发现,创建符号链接其权限就会显示为lrwxrwxrwx,为什么?是ls命令对符号链接进行了处理,还是文件本身权限即如此?这样会不会带来一些安全隐患?怀着这些问题,本文由浅入深,从ls命令出发,探索了其背后的系统调用至vfs文件系统实现细节,力求解释这些问题。但作者水平有限,尚有很多细节不清楚,不对之处恳请批评指正。
三、ls命令分析
ls命令是Linux shell下最常用的命令之一,主要用来列举目录下的文件信息。经过搜索引擎查找[1],要查看该命令的源代码需要下载对应软件包coreutils的源代码。其实只要知道了软件包的名字,既可以按照文献[1]的方法使用apt-get source下载,也可以从软件包coreutils的官网[2]下载。下载完毕后,使用source insight软件建立工程,即可方便地开始源码阅读。本文使用截止2017年2月18日最新版本的coreutils-8.26。
打开/src/ls.c,从main函数开始,忽略开始的初始化、颜色设定等内容,1451行调用的decode_switches对参数进行了一些处理,由于研究的是ls程序,所以第一个switch(ls_mode)关注LS_LS。接下来,1703行设置dereference = DEREF_UNDEFINED。关键部分代码为1752行的一个switch(true),它根据传入的参数,设置相应的标志,如-l设置format = long_format,H、I、L设置了dereference的一些模式,由于作者平时经常使用的是ls –l,所以仅关注-l选项下的情况,dereference仍然为DEREF_UNDEFINED。同时,该函数2125行对format == long_format的情况,做了一些格式输出上的工作。
发现1467行对dereference变量的判断影响了如何处理符号链接。若仅使用-l选项,dereference赋值为DEREF_NEVER,即仅仅拷贝复制符号链接自身。
若设置了递归枚举,设置一个哈希表来检测是否出现了目录环。接下来开辟cwd_file变量空间,其是指向fileinfo结构体的向量指针,保存了要描述的文件。关于fileinfo结构体源码中已经给了很好的注释,其中struct stat类型的变量stat具体描述了文件的信息,往往由stat()或lstat()函数返回。struct stat类型的定义可以在Linux源码include\\uapi\\asm-generic中找到,可以看出新版本64位中与常见文档中相比增加了许多pad填充,并将类型的一些宏定义取消了,直接采用了unsigned long。
下面主要调用了gobble_file函数,添加文件到当前的文件表中,即放到cwd_file中未使用的第一个位置上。3131行的switch语句根据dereference值,调用stat()或lstat()函数,由上面分析可知,ls –l是DEREF_NEVER,故调用lstat()函数,并将结果存入fileinfo.stat中(代码中是变量f);接下来,代码再根据结构体fileinfo.stat,对fileinfo其他部分赋值,在long_format情况下,fileinfo. scontext是SELinux有关函数lgetfilecon()获得的安全上下文(context)。同时也可以看出,默认-l选项是不对符号链接进行追踪的,所以调用的函数也都是对应版本。3235行判断当文件为符号链接且模式为long_format时会成立,由此赋值了fileinfo的linkok、linkmode等值。接下来函数主要根据要输出哪些信息,将fileinfo中的值保存了下来。
最后,在main函数1538行,根据文件的数目,调用print_current_files ()来输出文件内容,print_long_format()中第3967行通过filemodestring()函数将文件的读写执行权限填入了modebuf,该函数在filemode.c中定义。在填入时,ls程序未对符号链接做特殊处理,由此可见,符号链接权限问题的关键在于lstat()函数的实现是如何填入stat结构体中st_mode的。
四、lstat系统调用
4.0 系统调用与文件系统基础
4.0.1 从C语言到系统调用
stat是用来获得文件信息的系统调用[3],要寻找该系统调用的源代码,首先要理解系统调用的流程。这里参考了文献[4][5],根据自己理解,C语言调用stat函数时,调用的是C库对该函数的实现,接着执行库中函数的具体实现的代码,其中非常关键的一句代码就是 int 0x80,中断使得进程从用户态切换到内核态,中断处理程序然后开始执行内核中对应80号中断的系统调用处理程序的代码 system_call;system_call 系统调用处理程序就根据传入的系统调用号从系统调用服务程序数组中寻找对应系统调用服务程序,最后执行完成后按照调用顺序的相反顺序一步步返回结果。
根据一般规律,系统调用定义的名字就是在函数前面加一个sys_,由此在include\\linux\\syscalls.h中发现了一系列stat的声明,而fs\\Stat.c中是对应的定义。(内核中使用SYSCALL_DEFINE2的宏定义来定义系统调用,展开就是声明的形式。)这里会发现,4.10内核中同时存在newstat与stat,无论新旧,实现都是使用了vfs_stat函数,传入参数为kstat,差别在于宏倒数第二个参数的类型。(但是这个参数具体有什么作用?实现中好像并没有用到这个参数。)
接下来需要看vfs_lstat的实现,他与vfs_stat都是调用了vfs_fstatat,区别在于vfs_lstat给最后一个参数赋值为了AT_SYMLINK_NOFOLLOW,说明不要追踪符号链接。
vfs_fstatat首先对flag参数进行检查,必须有(并非等于)规定的几种标识之一;然后调用user_path_at,根据返回结果再调用vfs_getattr和path_put;之后看似是一个错误处理,会返回到retry,没有错误则函数退出返回。对关键数据stat赋值的部分应该就在vfs_getattr函数了。
4.0.2 文件系统基础
为了进行后面的分析,这里需要Linux内核文件系统有一定的了解[6][7]。Linux 有着极其丰富的文件系统,大体上可分如下几类:
网络文件系统,如 nfs、cifs 等;
磁盘文件系统,如 ext4、ext3 等;
特殊文件系统,如 proc、sysfs、ramfs、tmpfs 等。
实现以上这些文件系统并在 Linux 下共存的基础就是 Linux VFS(Virtual File System 又称 Virtual Filesystem Switch),即虚拟文件系统。VFS 作为一个通用的文件系统,抽象了文件系统的四个基本概念:文件、目录项 (dentry)、索引节点 (inode) 及挂载点,其在内核中为用户空间层的文件系统提供了相关的接口。VFS 实现了 open()、read() 、stat()等系统调并使得 cp 等用户空间程序可跨文件系统。VFS 真正实现了上述内容中:在 Linux 中除进程之外一切皆是文件。
Linux VFS 存在四个基本对象:超级块对象 (superblock object)、索引节点对象 (inode object)、目录项对象 (dentry object) 及文件对象 (file object)。超级块对象代表一个已安装的文件系统;索引节点对象代表一个文件;目录项对象代表一个目录项,如设备文件 event5 在路径 /dev/input/event5 中,其存在四个目录项对象:/ 、dev/ 、input/ 及 event5。文件对象代表由进程打开的文件。这四个对象与进程及磁盘文件间的关系如图,其中 d_inode 即为硬链接。为文件路径的快速解析,Linux VFS 设计了目录项缓存(Directory Entry Cache,即 dcache)。下面列出几个关键数据结构,并在关注的部分给出注释。
1 struct nameidata { //文件查找临时结构体 2 3 struct path path;//包含vfsmount挂载点和dentry目录项 4 5 struct qstr last; 6 7 struct path root; 8 9 struct inode *inode; /* path.dentry.d_inode */ 10 11 unsigned int flags; 12 13 unsigned seq, m_seq; 14 15 int last_type; /*路径中的最后一个component的类型*/ 16 17 unsigned depth; //符号链接嵌套的级别 18 19 int total_link_count; 20 21 struct saved { 22 23 struct path link; 24 25 struct delayed_call done; 26 27 const char *name; 28 29 unsigned seq; 30 31 } *stack, internal[EMBEDDED_LEVELS]; 32 33 struct filename *name; //保存要查找的文件名 34 35 struct nameidata *saved; 36 37 struct inode *link_inode; 38 39 unsigned root_seq; 40 41 int dfd; 42 43 }; 44 45 struct dentry {//目录项对象 46 47 /* RCU lookup touched fields */ 48 49 unsigned int d_flags; /* protected by d_lock */ 50 51 seqcount_t d_seq; /* per dentry seqlock */ 52 53 struct hlist_bl_node d_hash; /* lookup hash list */ 54 55 struct dentry *d_parent; /* parent directory */ 56 57 struct qstr d_name; 58 59 struct inode *d_inode; /* Where the name belongs to - NULL is 60 61 * negative */ 62 63 unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ 64 65 66 67 /* Ref lookup also touches following */ 68 69 struct lockref d_lockref; /* per-dentry lock and refcount */ 70 71 const struct dentry_operations *d_op;//目录项方法 72 73 struct super_block *d_sb; /* The root of the dentry tree */ 74 75 unsigned long d_time; /* used by d_revalidate */ 76 77 void *d_fsdata; /* fs-specific data */ 78 79 80 81 union { 82 83 struct list_head d_lru; /* LRU list */ 84 85 wait_queue_head_t *d_wait; /* in-lookup ones only */ 86 87 }; 88 89 struct list_head d_child; /* child of parent list */ 90 91 struct list_head d_subdirs; /* our children */ 92 93 /* 94 95 * d_alias and d_rcu can share memory 96 97 */ 98 99 union { 100 101 struct hlist_node d_alias; /* inode alias list */ 102 103 struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */ 104 105 struct rcu_head d_rcu; 106 107 } d_u; 108 109 }; 110 111 struct inode { //索引节点 112 113 umode_t i_mode; //文件类型与访问权限,是本文所关注的重点部分 114 115 unsigned short i_opflags;//2.6内核中没有的字段,哪里去找这个字段注释? 116 117 kuid_t i_uid; 118 119 kgid_t i_gid; 120 121 unsigned int i_flags; 122 123 124 125 #ifdef CONFIG_FS_POSIX_ACL 126 127 struct posix_acl *i_acl; 128 129 struct posix_acl *i_default_acl; 130 131 #endif 132 133 134 135 const struct inode_operations *i_op; //索引节点的操作 136 137 struct super_block *i_sb; 138 139 struct address_space *i_mapping; 140 141 142 143 #ifdef CONFIG_SECURITY 144 145 void *i_security; 146 147 #endif 148 149 150 151 /* Stat data, not accessed from path walking */ 152 153 unsigned long i_ino; 154 155 /* 156 157 * Filesystems may only read i_nlink directly. They shall use the 158 159 * following functions for modification: 160 161 * 162 163 * (set|clear|inc|drop)_nlink 164 165 * inode_(inc|dec)_link_count 166 167 */ 168 169 union { 170 171 const unsigned int i_nlink; 172 173 unsigned int __i_nlink; 174 175 }; 176 177 dev_t i_rdev; 178 179 loff_t i_size; 180 181 struct timespec i_atime; 182 183 struct timespec i_mtime; 184 185 struct timespec i_ctime; 186 187 spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */ 188 189 unsigned short i_bytes; 190 191 unsigned int i_blkbits; 192 193 blkcnt_t i_blocks; 194 195 196 197 #ifdef __NEED_I_SIZE_ORDERED 198 199 seqcount_t i_size_seqcount; 200 201 #endif 202 203 204 205 /* Misc */ 206 207 unsigned long i_state; 208 209 struct rw_semaphore i_rwsem; 210 211 212 213 unsigned long dirtied_when; /* jiffies of first dirtying */ 214 215 unsigned long dirtied_time_when; 216 217 218 219 struct hlist_node i_hash; 220 221 struct list_head i_io_list; /* backing dev IO list */ 222 223 #ifdef CONFIG_CGROUP_WRITEBACK 224 225 struct bdi_writeback *i_wb; /* the associated cgroup wb */ 226 227 228 229 /* foreign inode detection, see wbc_detach_inode() */ 230 231 int i_wb_frn_winner; 232 233 u16 i_wb_frn_avg_time; 234 235 u16 i_wb_frn_history; 236 237 #endif 238 239 struct list_head i_lru; /* inode LRU list */ 240 241 struct list_head i_sb_list; 242 243 struct list_head i_wb_list; /* backing dev writeback list */ 244 245 union { 246 247 struct hlist_head i_dentry; 248 249 struct rcu_head i_rcu; 250 251 }; 252 253 u64 i_version; 254 255 atomic_t i_count; 256 257 atomic_t i_dio_count; 258 259 atomic_t i_writecount; 260 261 #ifdef CONFIG_IMA 262 263 atomic_t i_readcount; /* struct files open RO */ 264 265 #endif 266 267 const struct file_operations *i_fop; /* former ->i_op->default_file_ops */ 268 269 struct file_lock_context *i_flctx; 270 271 struct address_space i_data; 272 273 struct list_head i_devices; 274 275 union { 276 277 struct pipe_inode_info *i_pipe; 278 279 struct block_device *i_bdev; 280 281 struct cdev *i_cdev; 282 283 char *i_link; 284 285 unsigned i_dir_seq; 286 287 }; 288 289 290 291 __u32 i_generation; 292 293 294 295 #ifdef CONFIG_FSNOTIFY 296 297 __u32 i_fsnotify_mask; /* all events this inode cares about */ 298 299 struct hlist_head i_fsnotify_marks; 300 301 #endif 302 303 304 305 #if IS_ENABLED(CONFIG_FS_ENCRYPTION) 306 307 struct fscrypt_info *i_crypt_info; 308 309 #endif 310 311 312 313 void *i_private; /* fs or device private pointer */ 314 315 }; 316 317 318 319 struct file { //文件对象,描述进程怎样与一个打开的文件进行交互 320 321 union { 322 323 struct llist_node fu_llist; 324 325 struct rcu_head fu_rcuhead; 326 327 } f_u; 328 329 struct path f_path; 330 331 struct inode *f_inode; /* cached value */ 332 333 const struct file_operations *f_op; 334 335 336 337 /* 338 339 * Protects f_ep_links, f_flags. 340 341 * Must not be taken from IRQ context. 342 343 */ 344 345 spinlock_t f_lock; 346 347 atomic_long_t f_count; 348 349 unsigned int f_flags; 350 351 fmode_t f_mode; 352 353 struct mutex f_pos_lock; 354 355 loff_t f_pos; //文件偏移 356 357 struct fown_struct f_owner; 358 359 const struct cred *f_cred; //进程相关安全上下文信息,如uid、权限等 360 361 struct file_ra_state f_ra; 362 363 364 365 u64 f_version; 366 367 #ifdef CONFIG_SECURITY 368 369 void *f_security; 370 371 #endif 372 373 /* needed for tty driver, and maybe others */ 374 375 void *private_data; 376 377 378 379 #ifdef CONFIG_EPOLL 380 381 /* Used by fs/eventpoll.c to link all the hooks to this file */ 382 383 struct list_head f_ep_links; 384 385 struct list_head f_tfile_llink; 386 387 #endif /* #ifdef CONFIG_EPOLL */ 388 389 struct address_space *f_mapping; 390 391 } __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
4.1 user_path_at函数
有了基础补充后,继续4.0.1节分析系统调用。user_path_at调用并返回user_path_at_empty的返回值,user_path_at_empty 返回filename_lookup的返回值,传参数时使用了getname_flags函数(实在看不懂这个函数,但是2.6内核要简洁的多,直接返回char*,4.10多设计了一个filename *)将用户传入的文件名const char __user *name转化为了struct filename *类型,其中__user宏定义为空的,可能是为了标记该参数由用户传入吧。(为什么要一层一层来调用,不怕效率低吗?)
filename_lookup主要调用了path_lookupat函数,在之前先调用了set_nameidata,对nameidata进行了一些初始化赋值,将要查询的文件名放入了结构体nameidata中,并且将从current取出的nameidata保存下来,组成了一个链表。(这里发现current是宏定义,开始的时候source insight自动追踪变成了循环宏定义,应该是追踪错了头文件,因为有许多个current.h。后来查阅资料看是获得调用系统调用进程的数据结构信息。)
4.1.1 path_lookupat函数
接着进入path_lookupat函数,此时参数为0 | LOOKUP_RCU,即LOOKUP_ RCU。RCU(Read-Copy Update)是内核数据同步的一种锁机制。
1.函数首先调用path_init()函数,path_init()函数主要是初始化查询,将nd实例的mnt和dentry成员设置为根目录或者工作目录的对应项
a,绝对路径(以/开始),获得根目录的dentry。它存储在task_struct中fs指向的fs_struct结构中。task_struct->fs_struct.root 。
b,相对路径,直接从当前进程task_struct结构中的获得指针fs,它指向的一个fs_struct,fs_struct中有一个指向“当前工作目录”的dentry。
2,path_lookupat()然后循环调用link_path_walk()函数。link_path_walk()函数将传入的路径名转化为dentry目录项:
首先跳过路径名的’/’,如果只有\'/\'则直接返回0;得到正确的路径名后,进入一个循环,每次都调用may_lookup()函数对inode节点做权限检查,如果权限不够也直接返回fail,在Unix中,只有目录是可执行的,它才可以被遍历;接下来计算的哈希与目录项高速缓存有关;该循环不断更新last_type和last,如果是最后一部分的返回,若不是则调用walk_component()函数。walk_component先处理LAST_DOTS,若发现LAST_NORM类型,即普通文件,则调用lookup_fast()在缓存中查找,若没有好的结果则调用lookup_slow(),获得i_mutex,重新检查缓存并向文件系统查找,他们都会调用follow_managed来处理挂载点;若有必要,期间walk_component会更新nameidata结构体的path。
link_path_walk会随着文件路径每部分深入,并追踪其间遇到的每个符号链接,直到其到达最后一部分,返回给nameidata.last。这个link_path_walk()函数本身非常复杂,也比较难懂,细节内容可参考文献[8][9][13],[13]对源码的注释非常清楚。link_path_walk函数主要是根据给定的路径,找到最后一个路径分量的目录项对象和安装点。
3. 在循环中还会调用trailing_symlink()函数来继续追踪最后部分的符号链接。trailing_symlink()会先调用may_follow_link(),这个函数检查符号链接的一些不安全权限情况。接着调用get_link(),先更新相关的访问时间等信息,然后调用inode中的get_link()方法完成符号链接解析;注意,原来的follow_link被get_link代替,而put_link,通过在get_link中设置set_delayed_call代替。
4. 最后对nd进行一些恢复收尾工作。
4.1.2 收尾
audit_inode调用了__audit_inode,定义在kernel/auditsc.c中,保存查找的inode和device,从名字猜测是审计用,这里先不关心。然后恢复current->nameidata,并释放name内存,与开头对应。这样filename_lookup结束,返回retval变量至user_path_at函数,即返回path_lookupat的结果。由此可见user_path_at就是检查是否存在这个文件,以及相关权限是否允许。
4.2 vfs_getattr函数
4.2.1 security_inode_getattr
security_inode_getattr定义在security/security.c,首先检查dentry的inode的i_flags是否为S_PRIVATE,即inode文件系统的安装标识。若不是,则调用call_int_hook(inode_getattr, 0, path),这个宏定义就看不懂了,涉及到security_hook_heads的很多东西,这里先忽略。
4.2.2 vfs_getattr_nosec
注释说明是在没有安全检查的情况下获得属性,即没有调用security_inode_getattr,实现很简单,从dentry目录项中获得inode,然后调用inode索引节点对象的getattr方法,再用generic_fillattr填充到返回的stat中,stat->mode=inode->i_mode。Inode方法中保存的函数指针,就指向了每个具体文件操作系统的的
以上是关于Linux下ls命令显示符号链接权限为777的探索的主要内容,如果未能解决你的问题,请参考以下文章