[apue] linux 文件訪問權限那些事兒


前言

說到 linux 上的文件權限,其實我們在說兩個實體,一是文件,二是進程。一個進程能不能訪問一個文件,其實由三部分內容決定:

  1. 文件的所有者、所在的組;
  2. 文件對所有者、組用戶、其它用戶設置的權限訪問位;
  3. 啟動進程的用戶、所在的組、有效用戶、有效用戶組。

下面先簡單說明一下這些基本概念,最后再說明它們是如何相互作用並影響進程訪問文件的。

用戶與組

用戶 ID 唯一標識一個登錄用戶,記錄在口令文件 (/etc/passwd) 中。ID 為 0 的用戶為超級用戶或根用戶 (root),具有繞過文件權限檢查的特權。

組 ID 用於將一類用戶組織在一起,記錄在組文件 (/etc/group) 中。下面這段 shell 腳本用來演示如何創建用戶並將它們添加到組中:

 1 #! /bin/bash
 2 useradd lippman
 3 useradd steven
 4 useradd caveman
 5 useradd paperman
 6 echo "create user ok"
 7 
 8 groupadd men
 9 groupadd share
10 echo "create group ok"
11 
12 usermod -a -G share lippman
13 usermod -a -G share steven
14 usermod -a -G men lippman
15 usermod -a -G men caveman
16 usermod -a -G men paperman
17 echo "add user to group ok"
18 
19 groups lippman steven caveman paperman
20 echo "show user and their group ok"
21 
22 groupdel men
23 groupdel share
24 echo "delete group ok"
25 
26 groups lippman steven caveman paperman
27 echo "show user and their group ok"
28 
29 userdel lippman
30 userdel steven
31 userdel caveman
32 userdel paperman
33 echo "delete user ok"
34 
35 rm -rf /home/lippman
36 rm -rf /home/steven
37 rm -rf /home/caveman
38 rm -rf /home/paperman
39 echo "remve user home dir ok"

這段腳本需要有管理員權限,請確保當前用戶為 root 用戶或屬於 sudoer 用戶組並使用 sudo 運行。下面是腳本的輸出:

$ sudo ./user_init.sh
create user ok
create group ok
add user to group ok
lippman : lippman men share
steven : steven share
caveman : caveman men
paperman : paperman men
show user and their group ok
delete group ok
lippman : lippman
steven : steven
caveman : caveman
paperman : paperman
show user and their group ok
delete user ok
remve user home dir ok

在你的機器上執行這段腳本的時候要特別小心,確保不會有同名的用戶或組已經存在,否則可能會將數據誤刪除。特別是刪除用戶時,用戶的工作目錄是不會一並刪除的,為了防止下次執行腳本時報警 (工作目錄已存在),這里同時刪除用戶的工作目錄 (line 35-38)。groups 命令為參數列表中的每個用戶羅列它們所在的組,一個用戶可以屬於多個組,它創建時所在的組稱為初始組,其它組稱為附加組,一個用戶最多可以添加的附加組數量上限可以通過 sysconf (_SC_NGROUPS_MAX) api 獲取 (或通過 getconf NGROUPS_MAX 命令獲取),在我的機器上這個值是 65536。關於系統限制值,可以參考我之前寫的這篇文章:《[apue] 一個快速確定新系統上各類限制值的工具 》。

從上面兩組高亮的輸出可以看出,附加組是可以先於用戶刪除的,刪除之后用戶就不在組中了。useradd 命令創建的用戶初始組名稱默認同用戶名,也可以通過 -g 參數指定一個已存在的組作為初始組,及通過 -G 參數指定一個或多個附加組,這與 usermod 命令的使用方式是相似的:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 groupdel men
16 groupdel share
17 echo "delete group ok"
18 
19 groups lippman steven caveman paperman
20 echo "show user and their group ok"
21 
22 userdel lippman
23 userdel steven
24 userdel caveman
25 userdel paperman
26 echo "delete user ok"
27 
28 rm -rf /home/lippman
29 rm -rf /home/steven
30 rm -rf /home/caveman
31 rm -rf /home/paperman
32 echo "remve user home dir ok"

與之前不同的是,為了在創建用戶時指定初始組,組的創建被放在了前面。這段腳本的輸出如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
groupdel: cannot remove the primary group of user 'caveman'
groupdel: cannot remove the primary group of user 'steven'
delete group ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok

可以看到通過 -G  添加組時,還是創建了默認的初始組 (lippman),而組在作為用戶的初始組存在的情況下是無法被刪除的 (line 15-16,附加組可以),所以最好調整一下刪除用戶和組的順序:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 userdel lippman
16 userdel steven
17 userdel caveman
18 userdel paperman
19 echo "delete user ok"
20 
21 rm -rf /home/lippman
22 rm -rf /home/steven
23 rm -rf /home/caveman
24 rm -rf /home/paperman
25 echo "remve user home dir ok"
26 
27 groupdel men
28 groupdel share
29 echo "delete group ok"

這樣再跑就沒問題了:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok
delete group ok

還有個有意思的點可以關注一下:

  • 刪除用戶時,用戶的初始組也會被一起刪除,但僅限該初始組沒有被其它用戶共享的情況下;
  • 單獨創建的附加組即使沒有包含任何用戶,也不會隨着最后用戶的刪除被自動刪除。

為了簡化后面的描述,將使用以下術語表示上面的概念:

  • 超級用戶: root
  • 用戶 ID:uid (user id)
  • 用戶組 ID:gid (group id)
  • 用戶初始組 (登錄組):initgrp (initial group)
  • 用戶附加組:supgrp (supplementary group)

與用戶和組相關的一些命令羅列如下:

  • 用戶:useradd / usermod / userdel / users
  • 用戶密碼:passwd / useradd | usermod -p
  • 用戶組:groupadd / groupmod / groupdel
  • 用戶組密碼:gpasswd / groupadd | groupmod -p  (你沒看錯,用戶組也可以有密碼)
  • 用戶與組的關系:id / groups / groupmems / usermod | useradd -g | G / gpasswd -a | -d
  • 用戶登錄:su / sudo / who / whoami / last / ac

這里需要強調的是通過 usermod 修改用戶組時,有三種方式:

  • usermod -a group1,group2... user:將 group[1-n] 添加到 user 的附加組中,原附加組保持不變;
  • usermod -G group1,group2... user:將 user 的附加組設置為 group[1-n],原附加組被清除;
  • usermod -g group user:將 user 的初始組設置為 group。

另外,像上面例子那樣,刪除用戶時需要同時刪除用戶 home 目錄的時候,只需要給 userdel 添加一個 -r 參數即可。有時除了 home 目錄,系統還會為新用戶創建郵件目錄,如果刪除用戶不清理這些目錄的話,再次創建的同名用戶就會告警,這里都可以通過 -r 參數一並刪除,避免后顧之憂。

后面我們會用這里創建的用戶及組來做一些驗證,具體就是在 line 14 插入一些測試腳本,用於驗證一些在多用戶場景下的權限問題。使用 su 命令可在多用戶之間切換,如果用戶設置了密碼,則在切換時要求輸入密碼,這里為了測試的便利性,都沒有給用戶帳號添加密碼,在實際場景中應避免這樣使用。

文件的用戶與組

文件本身有很多類型:

  • 普通文件
  • 目錄文件
  • 符號鏈接
  • 塊設備
  • 字符設備
  • FIFO
  • 套接字

所有文件都有創建者的 uid 和 gid,也有對應的文件權限位。針對普通文件,還可以再做一細分:

  • 可執行文件
  • 一般文件

可執行文件一般符合某種固定格式 (例如 elf),是進程的載體。針對這種文件,可以多設置兩種標志位:

  • 設置用戶 ID
  • 設置組 ID

它們決定了以該文件作為進程啟動時,新進程所使用的 uid 和 gid。對於 Solaris 系統,設置組 ID 也可以給普通的一般文件設置,不過含義也大為不同:表示啟用強制性文件記錄鎖,這是一種非標准擴展,不在本文的討論范圍,這里就不再展開說明了。

針對目錄文件,也可以多設置兩種標志位:

  • 設置組 ID
  • 粘住位 (sticky bit / svtx)

設置組 ID 與文件中的標志位相同,但是作用於目錄時,意義又不一樣了:表示該組下創建的文件的用戶組 ID 將追隨自己,而不是創建進程的組 ID,關於進程的組 ID 詳見下一節,關於目錄設置組 ID 位后新建文件的所有權,詳見“新建文件的權限”這一節;目錄加入粘住位時,會改變目錄默認的刪除文件、修改文件名的規則,具體見“進程訪問文件時內核權限檢查過程”這一節。

為了簡化后面的描述,將使用以下術語表示上面的概念:

  • 文件創建者(擁有者)用戶 ID:ouid (owner uid)
  • 文件創建者(擁有者)用戶組 ID:ogid (owner gid)
  • 文件權限位:perm (permission)
  • 文件設置用戶 ID:setuid
  • 文件設置組 ID:setgid
  • 文件粘住位:svtx (saved text bit)

需要注意的是文件沒有附加組的概念,它屬於上一節"用戶與組"的范疇,文件只能屬於一個用戶組,在創建時確定,不隨用戶的變更而變更,本節末尾有一個測試用例來驗證這一點。以上與文件相關的概念術語對應到文件的 stat 結構體的關系如下:

  • ouid:st_uid
  • ogid:st_gid
  • 文件類型:type = st_mode & S_IFMT
    • 普通文件:type & S_IFREG
    • 目錄文件:type & S_IFDIR
    • 符號鏈接:type & S_IFLNK
    • 塊設備:type & S_IFBLK
    • 字符設備:type & S_IFCHR
    • FIFO:type & S_IFFIFO
    • 套接字:type & S_IFSOCK
  • setuid:st_mode & S_ISUID
  • setgid:st_mode & S_ISGID
  • svtx:st_mode & S_ISVTX

這個結構體使用 stat / fstat / lstat 等 api 獲取,如果要獲取符號鏈接本身的屬性,需要使用 lstat,否則獲取的是符號鏈接指向的目標屬性。此外,還可以通過在 find 命令中指定參數來查找特定類型的文件,以上內容與 find 參數之間的對應關系如下:

  • 普通文件:-type f
  • 目錄文件:-type d
  • 符號鏈接:-type l
  • 塊設備:-type b
  • 字符設備:-type c
  • FIFO:-type p
  • 套接字:-type s

這些符號簡寫其實與 ls 輸出的文件類型是一致的 (每行第一個字符),關於 ls 的輸出例子,請參考后面和 find 結合查找文件的例子。三個額外的標志位通過 chmod 修改時,使用的關鍵字符如下:

  • setuid:chmod u+/-s
  • setgid:chmod g+/-s
  • svtx:chmod o+/-t

注意額外標志位是與固定的 u/g/o 權限組搭配的,關於權限組請參考“文件訪問權限位”一節。如果進行了錯誤的搭配,雖然不會報錯,但是也不會生效。由於這三個標志位是與執行權限放在一起的,所以最終顯示什么字符還與之前有沒有設置可執行 (x) 權限有關:

  • setuid+x:rws --- ---
  • setuid-x:rwS --- ---
  • setgid+x:--- rws ---
  • setgid-x:--- rwS ---
  • svtx+x : --- --- rwt
  • svtx-x :--- --- rwT

即小寫字母表示有執行權限,大寫表示沒有。也可以使用 find 搜索帶有特定標志位的文件,上面的內容與 find 搜索參數的對應關系為:

  • setuid:-perm -u+s
  • setgid:-perm -g+s
  • svtx:-perm -o+t

格式與 chmod 非常類似。其它的 rwx 權限位也都是可以搜索的,這里就不贅述了。下面我們用這個命令在測試機上搜索一些“特殊”的文件,首先看下 setuid 標志位:

$ find / -perm -u+s 2>/dev/null | xargs ls -ldh
-rwsr-xr-x 1 root root     52K Oct 31  2018 /usr/bin/at
-rwsr-xr-x 1 root root     73K Aug  9  2019 /usr/bin/chage
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chfn
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chsh
-rwsr-xr-x 1 root root     57K Aug  9  2019 /usr/bin/crontab
-rwsr-xr-x 1 root root     32K Oct 31  2018 /usr/bin/fusermount
-rwsr-xr-x 1 root root     77K Aug  9  2019 /usr/bin/gpasswd
-rwsr-xr-x 1 root root     44K Feb  3 00:31 /usr/bin/mount
-rwsr-xr-x 1 root root     41K Aug  9  2019 /usr/bin/newgrp
-rwsr-xr-x 1 root root     28K Apr  1  2020 /usr/bin/passwd
-rwsr-xr-x 1 root root     24K Apr  1  2020 /usr/bin/pkexec
---s--x--- 1 root stapusr 208K Oct 14  2020 /usr/bin/staprun
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/su
---s--x--x 1 root root    144K Jan 27 05:56 /usr/bin/sudo
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/umount
-rwsr-sr-x 1 abrt abrt     15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
-rwsr-x--- 1 root dbus     57K Sep 30  2020 /usr/libexec/dbus-1/dbus-daemon-launch-helper
-rwsr-xr-x 1 root root     16K Apr  1  2020 /usr/lib/polkit-1/polkit-agent-helper-1
-rwsr-xr-x 1 root root     11K Apr  1  2020 /usr/sbin/pam_timestamp_check
-rwsr-xr-x 1 root root     36K Apr  1  2020 /usr/sbin/unix_chkpwd
-rws--x--x 1 root root     40K Aug  9  2019 /usr/sbin/userhelper
-rwsr-xr-x 1 root root     12K Nov 17  2020 /usr/sbin/usernetctl

搜索到的全是普通文件,且是可執行文件,大部分位於 /usr/bin 下面,一般是超級用戶開放給普通用戶使用的命令。再看下 setgid 針對普通文件的情況:

$ find / -type f -perm -g+s 2>/dev/null | xargs ls -lh
-r-x--s--x 1 root slocate   40K Apr 11  2018 /usr/bin/locate
---x--s--x 1 root nobody   374K Aug  9  2019 /usr/bin/ssh-agent
-r-xr-sr-x 1 root tty       15K Jun 10  2014 /usr/bin/wall
-rwxr-sr-x 1 root tty       20K Feb  3 00:31 /usr/bin/write
-rwsr-sr-x 1 abrt abrt      15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
---x--s--x 1 root ssh_keys 455K Aug  9  2019 /usr/libexec/openssh/ssh-keysign
-r-x--s--x 1 root utmp      11K Jun 10  2014 /usr/libexec/utempter/utempter
-rwxr-sr-x 1 root root      11K Nov 17  2020 /usr/sbin/netreport
-rwxr-sr-x 1 root postdrop 214K Apr  1  2020 /usr/sbin/postdrop
-rwxr-sr-x 1 root postdrop 258K Apr  1  2020 /usr/sbin/postqueue

情況和 setuid 文件類似,一般是一些特殊的用戶組 (slocate / nobody / tty / postdrop …) 開放給普通用戶使用的命令。setgid 針對目錄時含義完全不同,所以這里限定了查找類型是普通文件,關於 setgid 目錄的例子,留到后面再說明。最后順便從上面的 ls 輸出看一下各列含義:

  • 第一列主要是 perm,其中也附帶顯示一些其它信息:
    • 第一個字母是類型,- 表示普通文件;
    • 之后分別是三組權限位,特殊標志也顯示在這里
  • 第二列為硬鏈接數;
  • 第三列為 ouid;
  • 第四列為 ogid;
  • 第五列為 size,對於目錄只表示目錄文件本身占用的空間,不代表目錄內文件占用總空間,想要顯示目錄占用總空間,需要使用 du 命令;
  • 第六列為最后修改日期;
  • 第七列為文件名。

使用 -h 選項使用 human readable 方式顯示文件大小——添加合適的單位 (K/M/G) 來讓人更易讀,否則直接顯示字節數;使用 -d 選項來打印目錄文件本身而不是列出目錄下的文件;ls 選項非常多,有興趣的同學可以自行 man 頁查看。

case:file_group_unchanged.sh

這個用例用來驗證文件的 ogid 在創建時確定,不隨用戶所屬用戶組的變更而變更。它由兩部分腳本組成,第一部分腳本中用戶將基於現在的組創建文件,在用戶切換所屬組后,第二部分腳本中將基於新切換的組再創建文件,並分別列出兩個文件的詳情,通過觀察它們的 ogid 來證明原文件的組不變。先來看第一部分腳本:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 touch this_is_a_test_file
7 ls -lh this_is_a_test_file

再來看第二部分腳本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 groups $(whoami) 
 7 echo "show user and their group ok"
 8 
 9 touch this_is_a_demo_file
10 ls -lh this_is_a*
11 
12 rm this_is_a*
13 echo "remove testing file ok"

最后來看框架腳本中添加的部分:

1     # case: file group unchanged
2     cp ./file_group_unchanged_1.sh /tmp/
3     cp ./file_group_unchanged_2.sh /tmp/
4     su - lippman -s /tmp/file_group_unchanged_1.sh
5     # change current owner's group
6     usermod -g share lippman
7     su - lippman -s /tmp/file_group_unchanged_2.sh
8     # change group back, otherwise we will got error on delete group
9     usermod -g lippman lippman

其中:

  • cp 命令將腳本放置在所有用戶可訪問的目錄 (默認位置為用戶私有工作目錄,其它用戶一般不能訪問),以便下一步做測試;
  • su 命令將以新用戶的身份運行腳本,這里使用了 lippman 用戶,當然也可以選用其它任何用戶,usermod -g 在 root 權限下執行時,可將任意用戶的 initgrp 設置為任意已存在的用戶組。
  • 夾在兩部分之間的腳本 (line 6) 用於切換用戶所屬的組。

這個用例需要使用兩個分開腳本的原因可以羅列如下:

  • 在腳本中調用 usermod 總是報錯,提示沒有權限 (即使只是將 initgrp 修改為 supgrp 中的一個也是如此);
  • 如果使用 sudo usermod,則需要將 lippman 加入 sudoer 文件才能起作用,但是那樣就感覺測試用例的可移植性差一些了;
  • usermod 命令修改用戶組之后,用戶需要重新登錄才能生效,這里每次 su 就相當於一次用戶登錄。

上面腳本的運行結果如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
switch to user lippman
lippman : share men
show user and their group ok
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
-rw-r--r-- 1 lippman share   0 May 30 21:13 this_is_a_demo_file
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

重點觀察新舊文件的 ogid 項,發現用戶切換組后,原文件的 ogid 不受影響,和預期的一致。

最后需要補充的一點是,su -s 選項用來基於新用戶身份執行一段腳本,而不能直接輸入 su username,否則會在腳本中執行過程中彈出交互式子 shell 從而導致執行被中斷。

進程的用戶與組

進程有比較多的用戶和組屬性:

  • 實際用戶 ID
  • 實際組 ID
  • 有效用戶 ID
  • 有效組 ID
  • 附加組 ID
  • 保存的設置用戶 ID
  • 保存的設置組 ID

讓我從進程的創建開始一一梳理:用戶的初始進程是由登錄 (login) 程序啟動的,它讀取 passwd 配置文件中該用戶對應的 uid 和 gid,作為用戶根進程的實際用戶 ID 和實際組 ID,用於標識進程是誰,一般在整個登錄會話過程中不會改變,當然超級用戶可以改變它們,這個話題超出了文章的范圍,放在以后說明。進程的有效用戶 ID 和有效組 ID 默認情況下與實際用戶 ID 和實際組 ID 一致,只有當出現以下情況時它們才不一致:  

  • 新進程的可執行文件有 setuid 標志且 ouid 與當前用戶不同;
  • 新進程的可執行文件有 setgid 標志且 ogid 與當前用戶不同。

場景一,進程的有效用戶 ID 被設置為可執行文件的 ouid;場景二,進程的有效組 ID 被設置為可執行文件的 ogid;兩個標志可以同時存在,亦可以同時生效 (網上有說法只有一個能生效是不對的,請看本節末尾的驗證用例)。

有效用戶 ID 與有效組 ID 是進程訪問文件時內核權限檢查的主要依據,具體的檢查過程請參考“進程訪問文件時內核權限檢查過程”這節。

進程的附加組 ID 即啟動進程用戶的附加用戶組 (supgrp),這個作為有效組 ID 的補充手段用於權限校驗,附加組 ID 中每個組都與有效組 ID 的作用等價 (即只要有一個附加用戶組匹配了文件 ogid,那么對應的權限就會生效)。沒有"設置附加組 ID" 這類的東西,所以附加組都是“原汁原味”不會改變的,這一點請看本節最后的驗證用例。

以我們耳熟能詳的 access 函數為例,它使用的是實際用戶 ID 與實際組 ID 進行訪問權限檢查,而不是有效用戶 ID 和有效組 ID,也就是說 access 返回失敗的文件,進程並不一定就不能訪問,這一點需要注意 (雖然沒什么用,因為你也不能確定它可以訪問)。書上有一個很好的例子,本節就不再畫蛇添足了,在“進程訪問文件時內核權限檢查過程”這節中你可以看到一個 shell 版本的 demo,演示了相同的功能。

為了簡化后面的描述,將使用以下術語表示上面的概念:

  • 進程實際用戶 ID:ruid (real uid)
  • 進程實際組 ID:rgid (real gid)
  • 進程有效用戶 ID:euid (effective uid)
  • 進程有效組 ID:egid (effective gid)
  • 進程附加組 ID:supgid (supplementary gid)
  • 進程保存的設置用戶 ID:save setuid
  • 進程保存的設置組 ID:save setgid

與進程 ID 相關 api 羅列如下:

  • ruid:getuid / setuid / setreuid / getresuid / setresuid
  • rgid:getgid / setgid / setregid / getresuid / setresuid
  • euid:geteuid / seteuid / setreuid / getresgid / setresgid
  • egid:getegid / setegid / setregid / getresgid / setresgid
  • supgid:getgroups / setgroups
  • save setuid:getresuid / setresuid
  • save setgid:getresgid / setresgid

set 部分一般需要嚴格的權限檢查,留在以后介紹進程關系時說明。save setuid / save setgid 和進程運行過程中執行 exec 相關,也不在這里展開。先來看 get 部分,有些 api 可以一次性獲取多個 id,所以在一個 ID 后面會跟多種獲取途徑。一般通過 ps 命令來顯示進程的各種 ID:

$ ps -axo pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 1031    81    81    81    81    81    81 -                    /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
 1037   998   997   998   997   998   997 997                  /usr/bin/lsmd -d
 5971   999   998   999   998   999   998 998                  /usr/lib/polkit-1/polkitd --no-debug
12357  1002  1003  1002  1003  1002  1003 1003                 sshd: yunh@pts/0
12465  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
14457    89    89    89    89    89    89 12,89                pickup -l -t unix -u
28301    89    89    89    89    89    89 12,89                qmgr -l -t unix -u
28632     0     0     0     0     0     0 -                    /usr/sbin/rsyslogd -n
28675     0     0     0     0     0     0 -                    /usr/sbin/crond -n
28720   997   995   997   995   997   995 -                    /usr/sbin/chronyd
28839    32    32    32    32    32    32 -                    /sbin/rpcbind -w

上面我們講的各種 ID 和 ps 命令的 format 參數 (-o) 及標題之間關系如下:

  • ruid:-o ruid / RUID
  • rgid:-o rgid / RGID
  • euid:-o euid / EUID
  • egid:-o egid / EGID
  • supgid:-o supgid / SUPGID
  • save setuid:-o suid / SUID
  • save setgid:-o sgid / SGID

ps 還可以展示許多其它的 ID,和本文關系不大,就不一一羅列了。

case:setuid_setgid_order.sh

這個用例用於驗證 setuid 和 setgid 可以同時作用於一個可執行文件,並且最終影響啟動的進程。這個例子由三段腳本組成,需要在框架腳本中添加如下代碼:

1     # case: setuid setgid order
2     cp ./setugid /tmp/
3     cp ./setuid_setgid_order_1.sh /tmp/
4     cp ./setuid_setgid_order_2.sh /tmp/
5     cp ./setuid_setgid_order_3.sh /tmp/
6     su - lippman -s /tmp/setuid_setgid_order_1.sh
7     su - caveman -s /tmp/setuid_setgid_order_2.sh
8     su - lippman -s /tmp/setuid_setgid_order_3.sh

其中 setugid 是一個可執行文件,啟動后 sleep 10 秒然后退出,主要是用來驗證啟動進程的一些屬性,比較簡單就不放源碼了; line 3-5 將三段腳本復制到公共目錄,原因同上;line 6-8 分別啟動三個用戶去執行腳本。第一個腳本用來准備 setuid / setgid  程序:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 # create setuid/setgid/setuid & setgid program
10 cp setugid setuid_demo 
11 chmod u+s,ugo+wx /tmp/setuid_demo
12 ls -lh setuid_demo
13 
14 cp setugid setgid_demo
15 chmod g+s,ugo+wx setgid_demo
16 ls -lh setgid_demo
17 
18 cp setugid setuid_setgid_demo
19 chmod ug+s,ugo+wx setuid_setgid_demo
20 ls -lh setuid_setgid_demo
21 
22 echo "create testing setuid/setgid file ok"

就是將 setugid 這個程序復制了三份,並分別設置了它們的 setuid / setgid / setuid & setgid 標志位。注意這里使用 id 打印了當前登錄用戶的各種 ID 值,這個在后面會用到。第二段腳本分別啟動三個進程,並打印它們的 ID 值:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setuid_demo &
10 ./setgid_demo &
11 ./setuid_setgid_demo &
12 
13 echo "start setuid/setgid program ok"
14 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
15 
16 echo "waiting them to exit..."
17 wait

重點就是 line 9-11 了,使用子進程的方式啟動,這樣可以同步打印 ps 的輸出結果 (line 14),在退出這段腳本前使用 wait 等待所有子進程結束。看到這里似乎就足夠了,那第三段腳本是用來做什么的呢?答案是清理剛才的可執行文件:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 rm setuid_demo
7 rm setgid_demo
8 rm setuid_setgid_demo
9 echo "remove testing file ok"

至於為什么要一個單獨的腳本來清理,稍后再說,這里先上腳本的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
-rwsrwxrwx 1 lippman lippman 9.4K May 31 20:51 setuid_demo
-rwxrwsrwx 1 lippman lippman 9.4K May 31 20:51 setgid_demo
-rwsrwsrwx 1 lippman lippman 9.4K May 31 20:51 setuid_setgid_demo
create testing setuid/setgid file ok
switch to user caveman
uid=1005(caveman) gid=1004(men) groups=1004(men)
start setuid/setgid program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
16113     0     0     0     0     0     0 0                    sudo ./user_init.sh
16124     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
16261     0     0     0     0     0     0 1004                 su - caveman -s /tmp/setuid_setgid_order_2.sh
16273  1005  1004  1005  1004  1005  1004 1004                 /bin/sh /tmp/setuid_setgid_order_2.sh
16275  1005  1004  1003  1004  1003  1004 1004                 ./setuid_demo
16276  1005  1004  1005  1006  1005  1006 1004                 ./setgid_demo
16277  1005  1004  1003  1006  1003  1006 1004                 ./setuid_setgid_demo
16278  1005  1004  1005  1004  1005  1004 1004                 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
waiting them to exit...
16275 exit
16277 exit
16276 exit
Last login: Mon May 31 20:51:09 CST 2021 on pts/0
switch to user lippman
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

首先各個文件的 setuid / setgid 位被正確設置了;其次 ps 的輸出可以看到,都是使用 uid / gid 的形式,這里就體現到了 id 命令的重要性,它已經提前將兩個用戶的 id 打印出來了,可以對號入座了:

  • setuid_demo:euid 1003 為 lippman,egid 1004 為 men;
  • setgid_demo:euid 1005 為 caveman,egid 1006 為 lippman;
  • setuid_setgid_demo:euid 1003 為 lippman,egid 1006 為 lippman;

全使用數字輸出可能有點亂,推薦將一個程序的 euid / egid  與它的 ruid / rgid 對比着來看,就能看出區別來:每個標志位都能單獨起作用,不存在誰生效了另外一個就不生效的問題。

需要注意的一點就是,不能使用 shell 腳本來充當 demo 進程,為 shell 腳本文件設置 setud / setgid 不會起作用,其實這個細想一下也可以想通——真正啟動的進程是 sh / bash 這類實體,shell 腳本文件只是它們解釋執行的數據文件。

最后來說為什么要將清理腳本單獨列出來,這是因為 caveman 沒有刪除文件的權限,如果合並到腳本二的話,會導致刪除失敗,所以有必要切回到創建文件的用戶再去刪除文件,關於刪除文件需要的權限,請參考“文件訪問權限位”一節; 關於 svtx 位設置后 (/tmp 目錄) 的刪除文件權限,請參考“進程訪問文件時內核權限檢查過程”一節。

case:process_supgid_unchanged.sh

這個用例主要用來驗證進程啟動后 supgid 不隨用戶 supgrp 改變而改變。這個例子由一段腳本組成,被用戶執行兩次,用戶在執行期間 supgid 發生了改變。需要在框架腳本中添加如下代碼:

1     # case: process groups unchanged
2     cp ./setugid /tmp/
3     cp ./process_supgid_unchanged.sh /tmp/
4     rm /tmp/should_wait 2>/dev/null
5     su - lippman -s /tmp/process_supgid_unchanged.sh
6     # change current owner's supplementary group
7     usermod -G lippman lippman
8     touch /tmp/should_wait
9     su - lippman -s /tmp/process_supgid_unchanged.sh

主要分為三步,先以當前附加用戶組啟動進程 (line 5),然后改變用戶的附加進程組 (line 7),最后以新的附加用戶組啟動進程 (line 9)。通過對比兩次啟動的進程 supgid 來觀察它們的差異。這里以用戶身份啟動一個腳本的方法與之前相同,不同的是設置了一個標志位文件 /tmp/should_wait 來標識是否需要等待啟動的進程,這也是研究了很多方法之后找到的一個解決方案,之前嘗試過使用環境變量、用戶配置文件 (~/.bash_profile),都達不到期望的效果。下面來看測試腳本的內容:

 1 #! /bin/bash
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setugid &
10 echo "start program ok"
11 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
12 
13 if [ -f "/tmp/should_wait" ]; then 
14     echo "waiting them to exit..."
15     wait
16 fi

主要就是啟動進程 (line 9),打印進程信息 (line 11),這個進程還是復用的上個用例中的 setugid 程序,主要是利用它啟動后 sleep 10 秒的時機通過 ps 來觀察一些進程的屬性。和之前修改用戶組一樣,修改了用戶的附加組信息后,需要用戶重新登錄才能生效,所以這里需要同樣的用戶執行兩次腳本。在第二次執行時,如果也不等待 demo 子進程結束就退出,會導致刪除用戶時報錯:

userdel: user lippman is currently used by process 4911

而第一次執行時又不能等待子進程 (需要保證舊的進程還運行時修改用戶附加組信息),所以這里使用了事先配置好的標志文件 (/tmp/should_wait) 來決定是否等待。下面看下腳本的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4892     0     0     0     0     0     0 1004,1005,1006       su - lippman -s /tmp/process_supgid_unchanged
 4909  1003  1006  1003  1006  1003  1006 1004,1005,1006       /bin/bash /tmp/process_supgid_unchanged.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4912  1003  1006  1003  1006  1003  1006 1004,1005,1006       ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
27258  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
28544  1002  1003  1002  1003  1002  1003 1003                 vim user_init.sh
Last login: Tue Jun  1 11:34:10 CST 2021 on pts/1
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4923     0     0     0     0     0     0 1006                 su - lippman -s /tmp/process_supgid_unchanged
 4934  1003  1006  1003  1006  1003  1006 1006                 /bin/bash /tmp/process_supgid_unchanged.sh
 4936  1003  1006  1003  1006  1003  1006 1006                 ./setugid
 4937  1003  1006  1003  1006  1003  1006 1006                 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
27258  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
28544  1002  1003  1002  1003  1002  1003 1003                 vim user_init.sh
waiting them to exit...
4911 exit
4936 exit
delete user ok
remve user home dir ok
delete group ok

重點看一下 setugid 進程的 SUPGID 信息,第一次啟動時,lippman 擁有三個附加組:1006 / 1004 / 1005 (通過 id 命令),啟動進程的 SUPGID 項和用戶 supgrp 內容一致; 第二次啟動前,通過 -G lippman 為用戶指定了唯一的一個附加組 1006,用戶的 supgrp 和新進程的 supgid 果然都變成了 1006,而舊進程的 supgid 仍保持三個不變 (新舊進程可以通過進程號區分)。

這個結論和用戶 gid 與文件 ogid 的關系非常類似,都是設置后不隨用戶的改變而改變了。最后解釋一下為什么沒有“設置用戶附加組 ID”這種東西,根源在於文件 inode 中沒有預留空間存儲 supgid,附加組都是進程啟動時從 /etc/group 中獲取的;另一方面,即使能存儲,這個用戶組權限匹配過程也會變得復雜,由 ogid -> egid + supgid 一對多的關系,變為多對多的關系,這么一整就亂套了。

文件訪問權限位

所有類型的文件都有訪問權限位,包括目錄,不過目錄的權限位與普通文件的權限位意義稍有不同,下面會詳細說明。權限位共有 9 個,按針對的用戶范圍分為三類:

  • 文件創建者
  • 文件創建者所在的用戶組 (該組的所有用戶,用戶組為該用戶的附加組也算)
  • 不在上面范圍的其它用戶

針對每類用戶,又有三類訪問權限:

  • 讀 (r)
  • 寫 (w)
  • 執行 (x)

它們規定了每類用戶具有的權限,如果申請的權限超過了給定的權限,訪問就會被拒絕。常用的一些操作及它們申請的權限羅列如下:

  • 打開文件
    • 路徑中的每個目錄:x
    • 路徑中的每個符號鏈接:- (無權限要求,主要看跳轉過程中所涉及到目錄和文件的權限)
    • 文件本身:
      • O_RDONLY:r
      • O_WRONLY:w
      • O_RDWR:rw
      • O_TRUNC:w
  • 創建文件
    • 路徑中的每個目錄:x
    • 路徑中的每個符號鏈接:- (無權限要求,主要看跳轉過程中所涉及到目錄和文件的權限)
    • 直屬目錄:wx
  • 刪除和重命名文件
    • 路徑中的每個目錄:x
    • 路徑中的每個符號鏈接:- (無權限要求,主要看跳轉過程中所涉及到目錄和文件的權限)
    • 直屬目錄:wx
    • 待刪除和重命名文件:- (無權限要求)
  • 執行文件 (通過 exec 函數族啟動進程):
    • 路徑中的每個目錄:x
    • 路徑中的每個符號鏈接:- (無權限要求,主要看跳轉過程中所涉及到目錄和文件的權限)
    • 文件本身:x
  • 列出目錄
    • 路徑中的每個目錄:x
    • 路徑中的每個符號鏈接:- (無權限要求,主要看跳轉過程中所涉及到目錄和文件的權限)
    • 目錄本身:r

需要對上面的目錄權限位做一些單獨說明:

  • 目錄執行權限位 (x) 也稱為搜索位,當一個目錄位於路徑的一部分時,如果用戶沒有目錄的執行位權限,則不能通過該目錄找到下一級文件或目錄,權限校驗直接失敗;
  • 對某個文件進行操作時,至少需要通過一個目錄,如果是絕對路徑,就是根目錄;如果是相對路徑,就是當前目錄,沒有指定 ./ 也會隱含通過當前目錄;
  • 對同一個文件,使用不同的路徑效果也會不同,例如 /usr/include/stdio.h,使用絕對路徑是一種方式,如果當前目錄位於 /usr/include/path/to/current/dir,則使用 ../../../../stdio.h 也是一樣的,但是如果 path/to/current/dir 中有任意一個目錄沒有搜索位 (x),則文件訪問就會失敗;反之亦然。這里主要是想強調一下“路徑中的每個目錄”的重要性,例子本身舉的比較牽強,畢竟那些目錄沒有搜索位的話,當前目錄也是不可能切 (cd) 過去的;

為了簡化后面的描述,將使用以下術語表示上面的概念:

  • 權限分組創建者:uperm (owner perm)
  • 權限分組創建者組:gperm (owner group perm)
  • 權限分組其它用戶:operm (other perm)
  • 讀權限:r (read)
  • 寫權限:w (write)
  • 執行/搜索權限:x (execute)

以上權限對應到 stat 中 st_mode 字段的關系如下:

  • uperm
    • r:S_IRUSR
    • w:S_IWUSR
    • x:S_IXUSR
    • rwx:S_IRWXU
  • gperm
    • r:S_IRGRP
    • w:S_IWGRP
    • x:S_IXGRP
    • rwx:S_IRWXG
  • operm
    • r:S_IROTH
    • w:S_IWOTH
    • x:S_IXOTH
    • rwx:S_IRWXO

使用 access 進行權限訪問時, mode 參數指定的標志位與權限對應關系如下:

  • R_OK:r
  • W_OK:w
  • X_OK:x
  • F_OK:- (只看文件是否存在,不檢查權限位)

四個標志位是可以組合使用的。shell 也有內建命令 (或者說選項) 來檢查文件的類型和訪問權限,其中文件類型使用的關鍵字和 ls、find 相同 (-f / -d / -l ...),權限方面則和 access 類似:

  • -r: r
  • -w: w
  • -x: x
  • -e: exist (不檢查類型,只檢查是否存在)

下節的測試用例演示了 shell 的內建權限檢查。

進程訪問文件時內核權限檢查過程

有了上面的基礎,再談進程訪問文件時的權限檢查過程就簡單多了:

  • euid == root,允許訪問
  • euid == ouid:
    • 申請的 perm <= uperm,允許訪問
    • 否則拒絕訪問
  • egid == ogid 或 supgid 包含 ogid 時:
    • 申請的 perm <= gperm,允許訪問
    • 否則拒絕訪問
  • 否則 (歸類於 other):
    • 申請的 perm <= operm,允許訪問
    • 否則拒絕訪問

這里需要注意幾點:

  • 檢查過程是“熔斷”的,即一個進程被歸類為文件的某個權限分組后,當該分組權限不滿足時,即使更低級別的權限分組允許,也不再向后嘗試,而是直接拒絕。舉個例子,如果某個文件權限位為 --- --- rwx,則只要進程屬於用戶的 owner 或 owner group,那么一定不允許訪問,反而是不在上面范圍內的其它用戶,可以獲得訪問權限,具體可參考本節末尾給出的測試用例;
  • 這個檢查過程和上一節中提到的文件路徑中每個目錄需要執行權限是不矛盾的,也就是說,完整的過程是對路徑中每個節點,依次執行本節說的檢查過程,不過呢,對於路徑中通過的目錄節點,申請的 perm 固定為 x 而已。舉個例子,想要讀取 /usr/include/stdio.h 文件,需要分四步(每步都需要執行上面完整的過程):
    • 檢查當前進程與 / 目錄的 x 權限;
    • 檢查當前進程與 /usr 目錄的 x 權限;
    • 檢查當前進程與 /usr/include 目錄的 x 權限;
    • 檢查當前進程與 /usr/include/stdio.h 文件的 r 權限。
  • 上一個例子擴展一下,如果是想執行 /usr/local/bin/sed 程序,目錄部分完全相同,只是最后一步有一些區別:
    • 檢查當前進程與 /usr/local/bin/sed 文件的 x 權限。
  • 如果想要在 /usr/local/bin 下創建新的可執行文件,則整個過程是這樣的:
    • 檢查當前進程與 / 目錄的 x 權限;
    • 檢查當前進程與 /usr 目錄的 x 權限;
    • 檢查當前進程與 /usr/local 目錄的 x 權限;
    • 檢查當前進程與 /usr/local/bin 目錄的 wx 權限;  
    • 新創建文件的權限與當前進程有關,這方面內容請參考“新建文件的權限”一節。
  • 如果要刪除或更名文件,大部分檢查過程和創建完全一樣,就像之前說過的,刪除文件和文件本身權限設置其實沒什么關系。

不過如果直屬目錄設置了 svtx 粘住位,則刪除、重命名文件的權限檢查過程稍有不同:

  • 有直屬目錄的 wx 權限,這個基本條件不變;
  • 還需要滿足以下條件:
    • euid == root
    • 或 euid == 文件 ouid
    • 或 euid == 直屬目錄 ouid

也就是說刪除、重命名 svtx 目錄下的文件時,要求非超級用戶進程必需擁有該文件或文件所在的直屬目錄,換個通俗的說法就是——你只能動你自己創建的文件,不能動其它人創建的的文件。我們熟知的 /tmp 目錄允許任何人在其中創建文件,而它就設置了 svtx 位。如果你要創建多個帳戶之間的共享目錄 (如 /share),使用 svtx 位是一個好習慣,具體可參考本節末尾的測試用例。

使用之前介紹過的 find 命令搜索帶有特殊標志位文件的辦法,來查找一下測試機上的 svtx 目錄,得到如下輸出:

$ find / -perm -o+t 2>/dev/null | xargs ls -ldh
drwxrwxrwt  2 root root   60 Mar 31 10:58 /dev/mqueue
drwxrwxrwt  2 root root   80 Apr 25 10:59 /dev/shm
drwxrwxrwt  2 root root 4.0K Mar 31 10:42 /matrix/matrix-bios/run
drwxr-xr-t  3 root root 4.0K May 22 16:36 /matrix/matrix-bios/var/state
drwxrwxrwt 12 root root 4.0K May 22 16:45 /tmp
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.font-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.ICE-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.Test-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.X11-unix
drwxrwxrwt  2 root root 4.0K Mar  4  2019 /tmp/.XIM-unix
drwxrwxrwt  2 root root 4.0K Oct 31  2018 /var/cache/coolkey
drwxrwxrwt  6 root root 4.0K May 22 13:00 /var/tmp

可以看到臨時目錄赫然在列。

case:perm_group_fuse.sh

這個腳本單純驗證以下權限組熔斷規則:uperm > gperm > operm,當訪問被歸類到某一級別的權限組后,就不再向低級別權限組探查。先來看探查權限的腳本:

 1 #! /bin/sh
 2 if [ $# -lt 1 ]; then 
 3     echo "Usage: probe_file_perm.sh file_to_test"
 4     exit 1
 5 fi
 6 
 7 filename=$1
 8 if [ -e "$filename" ]; then 
 9     echo "exist"
10 else
11     echo "not exist"
12     exit 1
13 fi
14 
15 if [ -r "$filename" ]; then 
16     echo "can read"
17 else
18     echo "can NOT read"
19 fi
20 
21 if [ -w "$filename" ]; then 
22     echo "can write"
23 else
24     echo "can NOT write"
25 fi
26 
27 if [ -x "$filename" ]; then 
28     echo "can execute"
29 else
30     echo "can NOT exeucte"
31 fi
32 
33 exit 0

其實就是 shell 版的 access,通過 strace 來觀察這段腳本的運行,發現其底層調用的 api 和 access 是一致的,所以這里所有的結論也適用於 access。下面來看調用點:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 echo "checking this_is_a_test_file"
 7 ./probe_file_perm.sh "this_is_a_test_file"
 8 echo ""
 9 
10 echo "checking this_is_a_demo_file"
11 ./probe_file_perm.sh "this_is_a_demo_file"
12 echo ""

對兩個 demo 文件分別進行權限測試,這兩個文件的創建請看這段腳本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -e this_is_a_test_file ]; then 
 7     touch this_is_a_test_file
 8     # '--- r-x -w-'
 9     chmod 0052 this_is_a_test_file
10     ls -lh this_is_a_test_file
11 fi
12 
13 if [ ! -e this_is_a_demo_file ]; then 
14     touch this_is_a_demo_file
15     # '--- r-x -w-'
16     chmod 0052 this_is_a_demo_file
17     ls -lh this_is_a_demo_file
18 fi
19 
20 echo "prepare testing file ok"

當文件不存在時,創建對應的文件並設置權限。注意這里權限比較特殊,0052 對應的權限位是 "--- r-x -w-",即 uperm 沒有任何權限、gperm 可讀可執行、operm 可寫,奇葩是夠奇葩的,不過這樣能很容易的根據最終訪問權限來確定命中了哪組權限位。最后上框架腳本中的驅動代碼:

 1     # case: permission group fusing
 2     cp ./probe_file_perm.sh /tmp/
 3     cp ./perm_group_fuse_1.sh /tmp/
 4     cp ./perm_group_fuse_2.sh /tmp/
 5     rm /tmp/this_is_a_test_file 2>/dev/null
 6     su - steven -s /tmp/perm_group_fuse_1.sh
 7     rm /tmp/this_is_a_demo_file 2>/dev/null
 8     su - caveman -s /tmp/perm_group_fuse_1.sh
 9     # start access test
10     su - steven -s /tmp/perm_group_fuse_2.sh
11     su - caveman -s /tmp/perm_group_fuse_2.sh
12     su - paperman -s /tmp/perm_group_fuse_2.sh
13     su - lippman -s /tmp/perm_group_fuse_2.sh

下面做個簡單說明:

  • line 2-4:准備測試腳本;
  • line 5-8:生成測試文件,注意這里為了讓兩個不同用戶使用同樣的腳本生成不同的文件,通過文件是否存在來控制文件的生成,最終 steven 用戶生成的是 this_is_a_test_file; caveman 用戶生成的是 this_is_a_demo_file;
  • line 10-13:讓各種用戶去測試這兩個文件的訪問權限。

ok,我們知道框架腳本中對用戶組的設定是這樣的:

  • men:caveman / paperman / lippman
  • share:steven / lippman
  • lippman:lippman

所以很容易就可以弄明白以下關系:

  • line 10:通過 steven 測試 test 文件的 uperm 與 demo 文件的 operm;
  • line 11:通過 caveman 測試 test 文件的 operm 與 demo 文件的 uperm;
  • line 12:通過 paperman 測試 test 文件的 operm 與 demo 文件的 gperm;
  • line 13:通過 lippman 測試 test 文件的 gperm 與 demo 文件的 gperm;

所有權限組基本上是都覆蓋到了,由於文件權限位的獨特性,可以得到 uperm 無權限;gperm 有讀和執行權限;operm 有寫權限,下面通過輸出來驗證一下:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user steven
----r-x-w- 1 steven share 0 Jun  1 19:58 this_is_a_test_file
prepare testing file ok
switch to user caveman
----r-x-w- 1 caveman men 0 Jun  1 19:58 this_is_a_demo_file
prepare testing file ok
Last login: Tue Jun  1 19:58:01 CST 2021 on pts/1
switch to user steven
checking this_is_a_test_file
exist
can NOT read
can NOT write
can NOT exeucte

checking this_is_a_demo_file
exist
can NOT read
can write
can NOT exeucte

Last login: Tue Jun  1 19:58:01 CST 2021 on pts/1
switch to user caveman
checking this_is_a_test_file
exist
can NOT read
can write
can NOT exeucte

checking this_is_a_demo_file
exist
can NOT read
can NOT write
can NOT exeucte

switch to user paperman
checking this_is_a_test_file
exist
can NOT read
can write
can NOT exeucte

checking this_is_a_demo_file
exist
can read
can NOT write
can execute

switch to user lippman
checking this_is_a_test_file
exist
can read
can NOT write
can execute

checking this_is_a_demo_file
exist
can read
can NOT write
can execute

delete user ok
remve user home dir ok
delete group ok

上面的數據可以整理成表格:

  this_is_a_test_file (steven) this_is_a_demo_file (caveman)
steven --- (uperm) -w- (operm)
caveman -w- (operm) --- (uperm)
paperman -w- (operm) r-x (gperm)
lippman r-x (gperm) r-x (gperm)

表格中每行表示一個用戶,每列表示一個文件,行列交叉處表示進程對文件的訪問權限。可見,每個進程的最終權限與之前清單中列出的預期是一致的,說明確實是熔斷了。例如以 steven 用戶進程訪問 test 文件為例,如果沒有發生熔斷,當 uperm 判定無權限后,那是不是應該退而求其次使用 gperm 判斷了?如此一來最終的訪問權限就變成了 r-x 而不是 ---。

 case:share_with_svtx.sh

這個測試用例用來驗證 svtx 目錄中,用戶不能刪除、重命名不屬於自己的文件,除非用戶擁有文件或直屬目錄。這個用例分為兩段腳本,第一段用來創建一些測試目錄和文件,第二段用來進行測試。首先看第一段腳本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -d /tmp/share ]; then 
 7     mkdir /tmp/share
 8     # allow every to create file 
 9     chmod ugo+rwx,o+t /tmp/share
10     ls -lhd /tmp/share
11 fi
12 
13 touch "/tmp/share/$(whoami)"
14 # 'rwx rw- r--'
15 chmod 0731 "/tmp/share/$(whoami)"
16 ls -lh "/tmp/share/$(whoami)"
17 
18 echo "prepare testing file ok"

這個腳本會被每個用戶執行一遍,因此在創建共享目錄 /tmp/share 之前,需要做個檢測 (line 6)。為了保證每個用戶都可以在共享目錄下創建文件,目錄的權限被設置為了 'rwx rwx rwt',其中 t 是為了驗證 svtx 位作用於目錄的效果 (line 9)。之后每個用戶在共享目錄下創建以自己命名的文件,文件權限設置為 'rwx rw- r--',以區分不同的權限分組 (line 14-16)。下面看下第二段腳本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # $1: file to move
 7 try_move_file ()
 8 {
 9     local file="$1"
10     local file_new="$1.new"
11     mv "$file" "$file_new" 2>/dev/null
12     if [ $? -eq 0 ]; then 
13         echo "can move"
14         # move back
15         mv "$file_new" "$file"
16     else
17         echo "can NOT move"
18     fi
19 }
20 
21 echo "checking /tmp/share/steven"
22 ./probe_file_perm.sh "/tmp/share/steven"
23 try_move_file "/tmp/share/steven"
24 echo ""
25 
26 echo "checking /tmp/share/caveman"
27 ./probe_file_perm.sh "/tmp/share/caveman"
28 try_move_file "/tmp/share/caveman"
29 echo ""
30 
31 echo "checking /tmp/share/paperman"
32 ./probe_file_perm.sh "/tmp/share/paperman"
33 try_move_file "/tmp/share/paperman"
34 echo ""
35 
36 echo "checking /tmp/share/lippman"
37 ./probe_file_perm.sh "/tmp/share/lippman"
38 try_move_file "/tmp/share/lippman"
39 echo ""

這個腳本也會被每個用戶分別執行,它對每個用戶創建在共享目錄下的文件挨個進行訪問權限檢查 (line 21-39),權限檢查是復用之前的 probe_file_perm.sh 腳本進行的;重命名檢查是通過 shell 函數 try_move_file 進行的;這里沒有對刪除進行測試,主要是一旦刪除成功,還需要重新創建文件以便下個用戶檢測,比較費事。重命名成功后,也需要將重命名后的文件再重命名回來,防止下個用戶找不到要檢測的文件,這個體現在 try_move_file 中了 (line 15)。最后將它們集成在框架腳本中:

 1     # case: share with svtx
 2     cp ./probe_file_perm.sh /tmp/
 3     cp ./share_with_svtx_1.sh /tmp/
 4     cp ./share_with_svtx_2.sh /tmp/
 5     su - steven -s /tmp/share_with_svtx_1.sh
 6     su - caveman -s /tmp/share_with_svtx_1.sh
 7     su - lippman -s /tmp/share_with_svtx_1.sh
 8     su - paperman -s /tmp/share_with_svtx_1.sh
 9     # start access test
10     su - paperman -s /tmp/share_with_svtx_2.sh
11     su - lippman -s /tmp/share_with_svtx_2.sh
12     su - caveman -s /tmp/share_with_svtx_2.sh
13     su - steven -s /tmp/share_with_svtx_2.sh
14     rm -rf /tmp/share

總體分為三步:復制腳本文件 (line 2-4);准備測試文件 (line 5-8);進行測試 (line 10-13);最后通過刪除共享目錄來清理測試文件。ok,看下腳本運行效果:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user steven
drwxrwxrwt 2 steven share 4.0K Jun  2 15:00 /tmp/share
-rwx-wx--x 1 steven share 0 Jun  2 15:00 /tmp/share/steven
prepare testing file ok
switch to user caveman
-rwx-wx--x 1 caveman men 0 Jun  2 15:00 /tmp/share/caveman
prepare testing file ok
switch to user lippman
-rwx-wx--x 1 lippman lippman 0 Jun  2 15:00 /tmp/share/lippman
prepare testing file ok
switch to user paperman
-rwx-wx--x 1 paperman men 0 Jun  2 15:00 /tmp/share/paperman
prepare testing file ok
Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user paperman
checking /tmp/share/steven
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/caveman
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/paperman
exist
can read
can write
can execute
can move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user lippman
checking /tmp/share/steven
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/caveman
exist
can NOT read
can write
can execute
can NOT move

checking /tmp/share/lippman
exist
can read
can write
can execute
can move

checking /tmp/share/paperman
exist
can NOT read
can write
can execute
can NOT move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user caveman
checking /tmp/share/steven
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/caveman
exist
can read
can write
can execute
can move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can NOT move

checking /tmp/share/paperman
exist
can NOT read
can write
can execute
can NOT move

Last login: Wed Jun  2 15:00:45 CST 2021 on pts/0
switch to user steven
checking /tmp/share/steven
exist
can read
can write
can execute
can move

checking /tmp/share/caveman
exist
can NOT read
can NOT write
can execute
can move

checking /tmp/share/lippman
exist
can NOT read
can NOT write
can execute
can move

checking /tmp/share/paperman
exist
can NOT read
can NOT write
can execute
can move

delete user ok
remve user home dir ok
delete group ok

創建的測試文件權限符合預期,第一個創建測試文件的用戶為 steven,它順便創建了共享目錄,因而目錄所有者就是 steven,而其它用戶只是自己文件的擁有者,注意這點區分,會對接下來的分析產生影響。進行文件權限檢查的輸出比較多,這里通過表格來展示:

  steven caveman lippman paperman
paperman --x (operm) -wx (gperm) --x (operm) rwx (uperm)
lippman -wx (gperm) -wx (gperm) rwx (uperm) -wx (gperm)
caveman --x (operm) rwx (uperm) --x (operm) -wx (gperm)
steven rwx (uperm) --x (operm) --x (operm) --x (operm)

這個表和之前用例中的表大同小異——每列表示一個文件,每行表示一個用戶,行列交叉處表示用戶對文件的訪問權限。由於文件名直觀的顯示了是由哪個用戶創建的,所以列標題得以簡化。與之前用例同理,根據最終訪問權限可以倒推出命中的是什么權限分組,而這也正好映射了兩個用戶之間的關系,即用戶訪問自己的文件,是 uperm;訪問同組用戶的文件,是 gperm;否則是 operm。

上表中得到的結論和實際一致嗎?大體是差不多的,但是可能有一些地方需要推敲一下,例如為何 paperman / caveman 訪問 lippman 的文件是 operm,而 lippman 訪問 paperman / caveman 的文件卻是 gperm,難道不應該都是 gperm 嗎?畢竟它們都屬於 men 組呀。這里再看一下它們創建的文件詳情:

-rwx-wx--x 1 lippman lippman 0 Jun  2 14:34 /tmp/share/lippman
-rwx-wx--x 1 caveman men 0 Jun  2 14:34 /tmp/share/caveman
-rwx-wx--x 1 paperman men 0 Jun  2 14:34 /tmp/share/paperman

不看不知道,一看嚇一跳,lippman 文件的 ogid 是 lippman;caveman / paperman 文件的 ogid 是 men。於是 lippman 用戶訪問 caveman 和 paperman 文件時,通過 supgid 中的 men 進行了匹配,所以是 gperm 權限組;反之,caveman / paperman 用戶訪問 lippman 文件時,沒有用戶組可以匹配 lippman 用戶組,於是只能命中 operm,這是導致訪問權限差異的根源。通過之前的章節我們知道,gid 是可以傳遞給文件並記錄下來的 (ogid),而 supgrp 是無法傳遞並記錄在文件中的 (只能記錄在進程的 supgid),所以,雖然用戶都在一個組,但是它們產生的文件可能並不在一個組。同理,lippman 訪問 steven 文件和 steven 訪問 lippman 文件的差異,也是由此而來。

為了清晰起見,下面單獨列出用戶是否可以重命名文件:

  steven caveman lippman paperman
paperman no no no yes
lippman no no yes no
caveman no yes no no
steven yes yes yes yes

只看前三行的話,是比較明確的——用戶只能重命名自己的文件,同組的用戶也不能相互重命名。再看第四行用戶 steven,它可以對所有人的文件進行重命名,這是為什么呢?如果大家還記得 steven 是目錄的創建者的話,就不會覺得驚訝了,這一條直接讓它符合了這條權限檢查規則:euid == 直屬目錄 ouid。另外可以在框架腳本中一行  rm -rf /tmp/share 刪掉所有文件包括共享目錄本身,也和另一條規則相關:euid == root。無意間將所有規則都演示了一遍,妙哇~

新建文件的權限

文件不會自己產生,它們都是由進程創建的。不論是通過命令還是 api,新建文件的權限與創建進程的屬性密切相關,具體影響規則如下:

  • ouid
    • <= euid
  • ogid
    • mac / freebsd <= 直屬目錄 ogid
    • Solaris:
      • 直屬目錄有 setgid 標志位 <= 直屬目錄 ogid
      • 否則 <= egid
    • linux:
      • 文件系統 mount 時指定了 grpid 或 bsdgroups 參數:
        • 直屬目錄有 setgid 標志位 <= 直屬目錄 ogid
        • 直屬目錄沒有 setgid 標志位 <= egid
      • 否則 (未指定或明確指定了 nogrpid 或 sysvgroups 參數) <= egid
  • perm
    • open 或 creat 時指定的 mode 參數
    • umask 指定的屏蔽字 (在最終的 perm 中關閉對應的位)

對於新文件的所有權,ouid 是比較明確的,就是繼承創建進程的 euid;ogid 稍微復雜一些,不過總的來說就兩個途徑:要么繼承進程的 egid、要么繼承直屬目錄的 ogid,具體需要按系統分情況來看。當然了,以上內容都源自 apue,作者成書較早,當時的系統版本都比較老,例如針對的 linux 平台還是 2.4 的內核,我用 CentOS 7.5 (內核 3.10) 驗證的時候發現有些出入——文件系統掛載 (mount) 時並未使用 grpid 或 bsdgroups 參數,但是也遵循上面的規則。即:

  • 直屬目錄有 setgid:ogid <= 直屬目錄 ogid
  • 否則:ogid <= 創建進程 egid

本節末尾的測試用例驗證了這一點,其它平台限於本文討論的范圍沒做驗證。可以把目錄的 setgid 理解成是強制繼承目錄的組權限,從而保證以該目錄為根節點的路徑樹對一組用戶有一致的訪問權限 (通過 ogid),而不會出現這樣的情況——雖然某個用戶進程屬於目錄 ogid 所在的組,但它是通過 supgid 加入此組的,而它自己的 egid 卻不在這個組,這樣它雖然可以在這個目錄下創建文件,但這個組的其它用戶卻不能訪問這些文件,從而導致這個目錄變得“支離破碎”。關於同組用戶卻不能創建同組文件的實例,請參考 “share_with_svtx” 用例。下面使用第一節介紹的 find 參數來查找一下測試機上的 setgid 目錄:

# find / -type d -perm -g+s 2>/dev/null | xargs ls -lhd
drwxr-sr-x  5 root systemd-journal 100 May 23 12:57 /run/log/journal
drwxr-sr-x  2 root systemd-journal  40 May 23 12:56 /run/log/journal/def
drwxr-s---+ 2 root systemd-journal 120 May 31 19:05 /run/log/journal/efe3d136ddc241e382a960b78ccc4718

得到的結果非常少,改變一下 ls 的選項,讓它遞歸列出目錄內容,看看這里面都有些什么:

# find / -type d -perm -g+s 2>/dev/null | xargs ls -lhR
/run/log/journal:
total 0
drwxr-x---  2 root root             60 Mar 31 10:41 86bac26592284276a583f8df03ff9a47
drwxr-sr-x  2 root systemd-journal  40 May 23 12:56 def
drwxr-s---+ 2 root systemd-journal 120 May 31 19:05 efe3d136ddc241e382a960b78ccc4718

/run/log/journal/86bac26592284276a583f8df03ff9a47:
total 8.0M
-rw-r----- 1 root root 8.0M Mar 31 10:41 system.journal

/run/log/journal/def:
total 0

/run/log/journal/efe3d136ddc241e382a960b78ccc4718:
total 168M
-rwxr-x---+ 1 root systemd-journal 56M Apr 16 14:40 system@6678dc3c22194c5190a67760a8fcc447-0000000000000001-0005becc0ba81976.journal
-rw-r-----+ 1 root systemd-journal 48M May  9 09:25 system@6678dc3c22194c5190a67760a8fcc447-000000000000d1f4-0005c0113fd51d85.journal
-rw-r-----+ 1 root systemd-journal 48M May 31 19:02 system@6678dc3c22194c5190a67760a8fcc447-0000000000017e6d-0005c1db87b877ab.journal
-rw-r-----+ 1 root systemd-journal 16M Jun  3 18:09 system.journal

/run/log/journal/def:
total 0

/run/log/journal/efe3d136ddc241e382a960b78ccc4718:
total 168M
-rwxr-x---+ 1 root systemd-journal 56M Apr 16 14:40 system@6678dc3c22194c5190a67760a8fcc447-0000000000000001-0005becc0ba81976.journal
-rw-r-----+ 1 root systemd-journal 48M May  9 09:25 system@6678dc3c22194c5190a67760a8fcc447-000000000000d1f4-0005c0113fd51d85.journal
-rw-r-----+ 1 root systemd-journal 48M May 31 19:02 system@6678dc3c22194c5190a67760a8fcc447-0000000000017e6d-0005c1db87b877ab.journal
-rw-r-----+ 1 root systemd-journal 16M Jun  3 18:09 system.journal

看起來絕大部分文件的確繼承了根目錄的 ogid,如果一個用戶只是通過 supgid 加入了 systemd-journal 組,那么它創建的 journal 文件也將可以被同組的其它用戶訪問到。

關於 setgid 最后再補充一點,新建文件類型為目錄時,遵循完全相同的規則,與普通文件唯一的不同是,當組 ID 繼承直屬目錄的 ogid 時,同時也會繼承它的 setgid 標志位,具體細節請參考節末用例。

最后再來說明一下 umask,在終端有一個同名的命令,可以打印當前 umask 值:

$ umask
0002

也可以用更直觀的方式打印:

$ umask -S
u=rwx,g=rwx,o=rx

這種情況下顯示的是最終保留的位。子進程一般會繼承父進程的 umask 值,不過子進程修改 umask 值不會影響父進程。

case:setgid_parent_dir.sh

這個用例用來驗證 setgid 作用於目錄時,目錄下的文件 ogid 將繼承目錄的 ogid 而不是創建者的 egid。這個用例只有一段腳本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 if [ ! -d /tmp/share ]; then 
 7     mkdir /tmp/share
 8     # allow everyone to create file 
 9     chmod ugo+rwx,g+s /tmp/share
10     ls -lhd /tmp/share
11 fi
12 
13 mkdir "/tmp/share/$(whoami)_dir"
14 touch "/tmp/share/$(whoami)_file"
15 ls -lhd /tmp/share/$(whoami)_*
16 
17 echo "prepare testing file ok"

這段腳本將以用戶身份運行,第一個進入的用戶負責創建 setgid 共享目錄 (line 6-11),目錄的 ogid 將追隨用戶的 egid;然后每個用戶在這個目錄下面創建一個目錄、一個普通文件,命名規則是“用戶名_file | dir”,然后列出它們以驗證 ogid 繼承了父目錄。注意 ls 的參數 (line 15) 不能用引號包圍,否則會報找不到文件錯誤,原因是 shell 通配符只在不被引號包圍的情況下才能生效。在框架腳本中加入啟動代碼:

1     # case: setgid parent dir
2     cp ./setgid_parent_dir.sh /tmp/
3     su - lippman -s /tmp/setgid_parent_dir.sh
4     su - caveman -s /tmp/setgid_parent_dir.sh
5     su - paperman -s /tmp/setgid_parent_dir.sh
6     su - steven -s /tmp/setgid_parent_dir.sh
7     #ls -lh "/tmp/share/"
8     rm -rf /tmp/share

挨個用戶執行該腳本。最終輸出如下:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsrwx 2 lippman lippman 4.0K Jun  3 21:13 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  3 21:13 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman 0 Jun  3 21:13 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
drwxr-sr-x 2 caveman lippman 4.0K Jun  3 21:13 /tmp/share/caveman_dir
-rw-r--r-- 1 caveman lippman 0 Jun  3 21:13 /tmp/share/caveman_file
prepare testing file ok
switch to user paperman
drwxr-sr-x 2 paperman lippman 4.0K Jun  3 21:13 /tmp/share/paperman_dir
-rw-r--r-- 1 paperman lippman 0 Jun  3 21:13 /tmp/share/paperman_file
prepare testing file ok
switch to user steven
drwxr-sr-x 2 steven lippman 4.0K Jun  3 21:13 /tmp/share/steven_dir
-rw-r--r-- 1 steven lippman 0 Jun  3 21:13 /tmp/share/steven_file
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

可以看到以下現象:

  • 共享目錄確實設置了 setgid 位 (rws) 且其 ogid 為創建者 egid (lippman);
  • 每個新創建的文件,不論普通文件還是目錄,ogid 繼承了共享目錄的 ogid (lippman) 而非創建用戶的 egid (men / share);
  • 每個新目錄也自動繼承了直屬目錄的 setgid 位 (r-s)。

一個用例驗證了兩個結論。現在回過頭來看目錄 setgid 的作用,它保證了這個目錄下的所有文件 (一般情況而言) 都有一致的 ogid,那這個有什么用處呢,下面分幾個方面來考察一下。

所有能在這個目錄下創建文件的用戶都能刪除、重命名別人的文件嗎?

是的。能創建文件說明進程有目錄 wx 權限,就能刪除和重命名文件,進一步的,如果有目錄 r 權限,還可以列出所有文件。

所有能在這個目錄下創建文件的用戶都能訪問目錄下的文件嗎?

不一定,文件雖然都有一樣的 ogid,但是還要看文件創建者對 gperm 的設置,例如上面默認的設置是 r-- (目錄 r-x),那么同組的人就只能讀、不能寫,創建者可以設置任意的 gperm 來允許或阻止同組人對自己文件的訪問,所以結論是:在 setgid 目錄下能創建文件的人,只能讀取、寫入或執行其它用戶願意讓你讀取、寫入或執行的文件。一般情況下是會允許同組用戶讀和執行的,不然也沒必要在共享目錄中創建文件了。

考察了 setgid 目錄的特性,再回到這個標志位的用處上來,前面其實已經簡單說過,但是這個問題書上沒有講,網上也很少有人涉及,還比較費思量,多廢一些口舌是值得的。如果僅僅是實現上面所說的功能,將需要訪問目錄的用戶加到同一個用戶組 (group id) 不就行了嗎?特別是有附加用戶組 (supgrp) 這種好東西,幾乎可以將一個用戶指派到無限的用戶組;然后將目錄 ogid 設置為這個組,這樣當用戶創建文件時,新文件的 ouid 跟隨進程 euid、ogid 跟隨進程的 egid 也就是目錄 ogid,不也能達到一樣的效果嗎?哎,等等,這里好像有什么地方不對,新文件的 ogid 是用戶進程的 egid 沒錯,但是不一定就是目錄 ogid 啊,為什么呢?因為用戶屬於這個組可能是通過附加用戶組 (supgrp),而不是初始用戶組 (initgrp),這樣一來,它創建的文件 ogid 將和父目錄的 ogid 不同。

這段話說的有點繞,舉個例子來說明,還以框架腳本中的用戶為例,如果父目錄 ogid 為 men,steven 不在對應的組,沒有訪問權限;caveman / paperman / lippman 在這個組中,都能創建文件;其中 caveman / paperman 在目錄中創建文件的 ogid 跟隨自己的 egid 也將為 men;而 lippman 創建的文件 ogid 將為 lippman 而非 men,從而在這個目錄下面制造了一個“另類”,這個文件另類到其它同組用戶可能根本沒有訪問權限——即使設置了 gperm 的適當權限也不行,除非放開 operm 權限,但是那樣又會導致文件被任意不相干的人訪問,這不是我們期望的。

所以刨根究底,目錄 setgid 其實是為了填用戶附加組 (supgrp) 挖的坑,讓幾乎不設限制的把一個用戶添加到附加組這種行為,最終能得到期望的執行效果……

那么,最終要如何設置某個組的共享目錄才是合理的合理的呢?以下幾個是需要注意的點:

  • 目錄 ogid 必需為要共享的組的 gid;
  • 目錄最好設置 setgid;
  • 有額外要求的話 (禁止刪除、重命名非自己創建的文件),可以設置 svtx 位。

之后,如果有哪個用戶需要加入共享組,直接將這個組添加到他的附加組並重新登錄即可,反之,將用戶從組中刪除即可。

更改文件權限

文件權限的變更主要分為兩部分,一是文件所有權 (ouid / ogid) 的變更; 一是文件訪問權限的變更 (perm)。下面分別說明。

變更文件所有權

文件所有權可通過 chown / chgrp 命令或基於 chown / fchown / lchown api 變更,最終能否設置新的所有權由以下規則決定:

  • euid == root
  • 或 euid == ouid
    • new owner == -1 或 new owner == ouid
    • 且 new group == -1 或 new group == ogid 或 supgid 包含 new group

即超級用戶可以將任意文件更改到任意用戶;而文件 owner (或 setuid 后相當於文件 owner) 進程只可以將屬於自己的文件 ogid 更新到本人所屬的其它組 (egid 和 supgid 組成的范圍)。其中 -1 在 api 接口中表示對應字段不變更,相當於忽略對應的字段。當然並不是所有平台都是這樣,sysv 系統和 freebsd 稍有不同:

  • freebsd / mac 都遵循和 linux 一樣的規則:只允許 root 用戶修改文件到任意用戶;
  • Solaris 默認配置和 linux 一樣,但是也可以通過修改配置來遵循 sysv 的規則:允許任意用戶修改自己擁有的文件到其它用戶。

不允許非 root 用戶修改文件所有權的目的,據書上的說法是為了防止用戶擺脫磁盤空間限額,這里限於文章范圍,沒有做驗證。非超級用戶更改文件所有權后,普通文件的特殊標志位也會跟着變化:

  • setuid 標志位:清除
  • setgid 標志位:清除

為什么在更新文件所有權后要清除這兩個位呢?書上沒有細說,感覺主要是 chown 后 setgid 代表的用戶組發生了變更,防止誤用。關於這一點可以參考本節末尾的用例。

下面簡單了解一下 chown 與 chgrp 的用法:

chown user:group file
chown user file
chown :group file
chgrp group file

chown 可同時變更文件的 ouid 與 ogid,使用冒號分隔用戶名與組名。如果只修改其中一個,另一個可以留空,特別當用戶組為空時,冒號可以省略;chgrp 相對“單純”一些,只能修改文件的 ogid。詳細的選項可以參考 man 手冊頁。

變更文件訪問權限

文件訪問權限可通過 chmod 命令或基於 chmod / fchmod api 變更,最終能否設置新的權限由以下規則決定:

  • euid == root
  • 或 euid == ouid

即只有超級用戶、文件 owner (或 setuid 后相當於文件 owner) 進程可以更改文件的權限。新的文件權限由 mode 參數指定,這里的 mode 參數和 open / creat 一致。注意沒有 lchmod,也就是說符號鏈接本身的權限基本是被忽略的,沒有修改的必要,對此有疑問的同學可參考 man 手冊頁中的這段描述:

       chmod  never  changes  the permissions of symbolic links; the chmod system call cannot
       change their permissions.  This is not a problem since  the  permissions  of  symbolic
       links  are  never  used.   However, for each symbolic link listed on the command line,
       chmod changes the permissions of the pointed-to file.  In contrast, chmod ignores sym‐
       bolic links encountered during recursive directory traversals.

在 mac 上符號鏈接的權限是可以被修改的 (通過 -h 選項),不過即使將符號鏈接的權限都關閉,仍可以通過它找到目標文件,僅是 readlink 出錯不能讀取符號鏈接內容而已,所以將符號鏈接的權限當成空氣就好了。

除了可以設置權限位以外,還可以設置特殊標志位。特殊標志位除了遵循上面通用的規則,還有自己特定的規則,下面分別說明:

svtx 位

對於非超級用戶設置普通文件的 svtx 標志位,不同系統有不同的策略:

  • freebsd / mac / Solaris:忽略 (只允許 root 設置)
  • linux:允許 (其實設置了也沒什么效果)

限於本文范圍,只在 linux 上做了驗證,可參考本節末尾的用例。這個 svtx 位 (作用於普通文件) 在很早之前是為了減少頻繁啟動程序 (例如 vi / gcc) 的內存交換而設計的一種機制 (程序退出后仍保留在內存中以便下次快速加載),現在隨着操作系統內存管理天翻地覆的改變,早已不再使用了,如果不是后來擴展出了作用於目錄的用法,這個標志都不可能保留到現在。對於還支持這個標志位的系統 (例如 Solaris),它的底層機制也完全不同了,可能使用了某種高速緩存機制;不過普通文件如果沒有可執行位,那么系統也不會高速緩存它們,因為這個標志位只針對普通文件中的可執行文件生效。

目錄 setgid 位

在 setgid 目錄中創建文件時,新文件的 ogid 會追隨直屬目錄而非創建進程 (見“新建文件的權限”一節),上一節末尾的用例中提到當文件類型為目錄時,新目錄會繼承父目錄的 setgid 位。而在以下條件成立時,新目錄的 setgid 繼承會被自動關閉:

  • euid != root
  • 且新文件 ogid != egid
  • 且新文件 ogid 不包含於 supgid

不論新目錄有沒有 setgid 位,它的 ogid 必定是直屬目錄的 ogid 沒錯了,所以並不是直屬目錄的 setgid 沒起作用,而是不再向下傳遞了而已。那為什么要設計這么一條規則呢? 書上的說法是——“防止了用戶創建一個設置組 ID 文件,而該文件是由並非用戶組所屬的組擁有的”,聽的雲里霧里的,不過就規則本身來說可以這樣理解:有一個 setgid 的共享目錄,可以訪問它的人可分為三種,第一類是 root 或 owner,一般擁有最高權限;第二類是組成員,用戶的 egid 或 supgid 中的一個必定可以匹配目錄的 ogid;第三類是其它用戶,就是不在上面范圍的用戶。從規則的描述上來看,第一類人基本不受權限制約,被排除,由第二三條規則可知也不是第二類人,所以規則其實指的就是第三類人,即目錄 operm 所描述的用戶集合。此時如果 operm 指定的權限允許他們創建目錄,則新創建的目錄是不會自動繼承 setgid 標志位的。這個還是比較容易驗證的,以 setgid_parent_dir.sh 為例,再溫習一下它的輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsrwx 2 lippman lippman 4.0K Jun  7 20:28 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  7 20:28 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman    0 Jun  7 20:28 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
drwxr-sr-x 2 caveman lippman 4.0K Jun  7 20:28 /tmp/share/caveman_dir
-rw-r--r-- 1 caveman lippman    0 Jun  7 20:28 /tmp/share/caveman_file
prepare testing file ok
switch to user paperman
drwxr-sr-x 2 paperman lippman 4.0K Jun  7 20:28 /tmp/share/paperman_dir
-rw-r--r-- 1 paperman lippman    0 Jun  7 20:28 /tmp/share/paperman_file
prepare testing file ok
switch to user steven
drwxr-sr-x 2 steven lippman 4.0K Jun  7 20:28 /tmp/share/steven_dir
-rw-r--r-- 1 steven lippman    0 Jun  7 20:28 /tmp/share/steven_file
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

共享目錄 share 是由 lippman 創建的,所以它的 owner 是 lippman,組也是 lippman,而其它用戶無論 ogid 還是 supgid,都不屬於 lippman 組,因此符合上面所說第三類人的條件,但是輸出顯示它們創建的目錄卻都繼承了 setgid 位,可見書中這里描述有誤。為了驗證這一點,修改 setgid_parent_dir.sh 中一行設置代碼:

chmod ug+rwx,g+s /tmp/share

去掉了 o 來關閉對其它用戶的寫入權限,再執行一下腳本,得到如下輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
drwxrwsr-x 2 lippman lippman 4.0K Jun  7 20:31 /tmp/share
drwxr-sr-x 2 lippman lippman 4.0K Jun  7 20:31 /tmp/share/lippman_dir
-rw-r--r-- 1 lippman lippman    0 Jun  7 20:31 /tmp/share/lippman_file
prepare testing file ok
switch to user caveman
mkdir: cannot create directory '/tmp/share/caveman_dir': Permission denied
touch: cannot touch '/tmp/share/caveman_file': Permission denied
ls: cannot access /tmp/share/caveman_*: No such file or directory
prepare testing file ok
switch to user paperman
mkdir: cannot create directory '/tmp/share/paperman_dir': Permission denied
touch: cannot touch '/tmp/share/paperman_file': Permission denied
ls: cannot access /tmp/share/paperman_*: No such file or directory
prepare testing file ok
switch to user steven
mkdir: cannot create directory '/tmp/share/steven_dir': Permission denied
touch: cannot touch '/tmp/share/steven_file': Permission denied
ls: cannot access /tmp/share/steven_*: No such file or directory
prepare testing file ok
delete user ok
remve user home dir ok
delete group ok

其它用戶果然都無法創建任何文件了,可見之前的推論是沒問題的。

文件 setuid / setgid  位

對於非超級用戶進程寫入一個文件時,以下特殊標志位會自動關閉:

  • setuid 標志位:清除
  • setgid 標志位:清除

這一策略主要是為了防止一些黑客篡改帶有 setuid / setgid 位的可執行文件,讓它們以普通用戶身份獲得超級用戶權限去執行一些惡意代碼來干壞事。本節末尾有一個用例演示了這一點。

如果使用 truncate / ftruncate 代替 write 來“寫”文件時,得到的結果相同。

case:chgrp_clear_setgid.sh

這個用例主要用來驗證變更文件 ouid 或 ogid 后,文件的 setuid 與 setgid 標志位會被清除。它由一段腳本組成:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 touch "/tmp/this_is_a_demo_file"
 7 chmod ug+s,o+t "/tmp/this_is_a_demo_file"
 8 ls -lh "/tmp/this_is_a_demo_file"
 9 
10 mkdir "/tmp/this_is_a_demo_dir"
11 chmod ug+s,o+t "/tmp/this_is_a_demo_dir"
12 ls -lhd "/tmp/this_is_a_demo_dir"
13 
14 #chown lippman:men "/tmp/this_is_a_demo_file"
15 #chown :men "/tmp/this_is_a_demo_file"
16 chgrp men "/tmp/this_is_a_demo_file"
17 #chgrp share "/tmp/this_is_a_demo_file"
18 #chgrp lippman "/tmp/this_is_a_demo_file"
19 ls -lh "/tmp/this_is_a_demo_file"
20 
21 chgrp share "/tmp/this_is_a_demo_dir"
22 ls -lhd "/tmp/this_is_a_demo_dir"
23 
24 echo "chgrpp clear setgid over"

主要就是創建普通文件 (line 6-8)、創建目錄 (line 10-12)、變更文件所有權 (line 16,21)、檢查標志位 (line 19,22),這里同時使用普通文件和目錄文件作為對比。修改文件所有權的方式有很多,這里列舉了四種方法 (line 14-16),選擇了最直觀的 chgrp 方式。此處只可將文件 ogid 設置為進程 egid + supgid 范圍內的用戶組 (line 16-18 men / share / lippman),否則報錯:

chgrp: changing group of '/tmp/this_is_a_demo_file': Operation not permitted

在框架腳本中合適的位置插入代碼來啟動這個用例:

 1     # case: change ogid clear setgid
 2     rm /tmp/this_is_a_demo_file 2>/dev/null
 3     rm -rf /tmp/this_is_a_demo_dir 2>/dev/null
 4     cp ./chgrp_clear_setgid.sh /tmp/
 5     su - lippman -s /tmp/chgrp_clear_setgid.sh
 6     chmod ug+s,o+t "/tmp/this_is_a_demo_file"
 7     chown caveman "/tmp/this_is_a_demo_file"
 8     ls -lh "/tmp/this_is_a_demo_file"
 9     chmod ug+s,o+t "/tmp/this_is_a_demo_dir"
10     chown steven "/tmp/this_is_a_demo_dir"
11     ls -lhd "/tmp/this_is_a_demo_dir"

這里使用 lippman 用戶執行上面的用例,因為他同時屬於三個用戶組,可以在這之間做無縫切換,便於驗證。

上面那個腳本只能驗證變更文件 ogid,從本節前面的內容可以得知,想要變更文件 ouid,必需使用 root 權限,剛好框架腳本具有 root 權限,於是在后半部分順便驗證了文件 ouid 的變更對特殊標志位的影響 (line 6-11)。上面腳本的輸出如下:

 1 $ sudo ./user_init.sh  2 create group ok  3 create user ok  4 lippman : lippman men share  5 steven : share  6 caveman : men  7 paperman : men  8 show user and their group ok  9 switch to user lippman 10 -rwSr-Sr-T 1 lippman lippman 0 Jun 7 19:12 /tmp/this_is_a_demo_file 11 drwsr-sr-t 2 lippman lippman 4.0K Jun 7 19:12 /tmp/this_is_a_demo_dir 12 -rw-r-Sr-T 1 lippman men 0 Jun 7 19:12 /tmp/this_is_a_demo_file 13 drwsr-sr-t 2 lippman share 4.0K Jun 7 19:12 /tmp/this_is_a_demo_dir 14 chgrpp clear setgid over 15 -rw-r-Sr-T 1 caveman men 0 Jun 7 19:12 /tmp/this_is_a_demo_file 16 drwsr-sr-t 2 steven share 4.0K Jun 7 19:12 /tmp/this_is_a_demo_dir 17 delete user ok 18 remve user home dir ok 19 delete group ok

由於創建的是普通文件,所以初始權限為 "rw- r-- r--",經過 chmod 處理后變為  "rwS r-S r-T",從前面章節可以知道,這里 S 分別表示 setuid 與 setgid 沒有 x 權限位的組合,T 是 svtx 沒有 x 權限的表示;相對的,由於目錄的初始權限為 "rwx r-x r-x",經過 chmod 處理后就變為 “rws r-s r-t”,上面的輸出符合預期。第一次變更普通文件 ogid,從 lippman 變更為了 men,再觀察文件權限位,發現 setuid 確實被清除了,setgid 卻仍然保留;第二次是變更普通文件用戶 ouid,從 lippman 變更為 caveman,結果與前面相同。難道 x 權限位的缺失導致了 setgid 沒有正確清除?抱着試一試的態度,將代碼中 chmod 的參數修改如下:

chmod ug+xs,o+xt "/tmp/this_is_a_demo_file" chmod ug+xs "/tmp/this_is_a_demo_dir"

為文件的所有 perm 分組加入了 x 權限,此時腳本輸出如下:

create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rwsr-sr-t 1 lippman lippman 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 lippman lippman 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
-rwxr-xr-t 1 lippman men 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 lippman share 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
chgrpp clear setgid over
-rwxr-xr-t 1 caveman men 0 Jun  7 19:25 /tmp/this_is_a_demo_file
drwsr-sr-t 2 steven share 4.0K Jun  7 19:25 /tmp/this_is_a_demo_dir
delete user ok
remve user home dir ok
delete group ok

 所有 S 變成了 s,且在變更文件 ogid / ouid 后,都能正確的將 setuid 與 setgid 位清除了。從上面的輸出還可以得到以下結論:

  • 作用於普通文件的 svtx (沒毛用) 不受影響;
  • 作用於目錄的 svtx 不受影響;
  • 作用於目錄的 setuid (沒毛用) 不受影響;
  • 作用於目錄的 setgid 不受影響;

case:write_clear_setuid.sh

這個用例是用來驗證 write 或 truncate 后,可執行文件的 setuid 和 setgid 標志位將被清除。它由一段腳本組成:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # create setuid & setgid program
 7 cp setugid setugid_demo
 8 chmod ug+s,ugo+wx /tmp/setugid_demo
 9 ls -lh setugid_demo
10 
11 echo "create testing setuid/setgid file ok"
12 echo "1" >> setugid_demo
13 
14 echo "write 1 bytes into executable file"
15 ls -lh setugid_demo
16 
17 chmod ug+s,ugo+wx /tmp/setugid_demo
18 truncate -s 8K setugid_demo
19 
20 echo "truncate executable file to 8K"
21 ls -lh setugid_demo
22 
23 rm setugid_demo
24 echo "remove testing file ok"

主要分以下幾步:創建帶 setuid / setgid 標志的可執行文件 (line 7-9)、在文件末尾寫入一字節觀察結果 (line 11-15)、重新設置標志位並截斷文件再觀察結果 (line 17-21)。在框架腳本中加入以下啟動代碼:

1     # case: write clear setuid 
2     cp ./setugid /tmp/
3     cp ./write_clear_setuid.sh /tmp/
4     su - lippman -s /tmp/write_clear_setuid.sh

用 lippman 身份執行該腳本。下面是腳本輸出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rwsrwsrwx 1 lippman lippman 9.5K Jun  8 09:48 setugid_demo
create testing setuid/setgid file ok
write 1 bytes into executable file
-rwxrwxrwx 1 lippman lippman 9.5K Jun  8 09:48 setugid_demo
truncate executable file to 8K
-rwxrwxrwx 1 lippman lippman 8.0K Jun  8 09:48 setugid_demo
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

初始時測試文件是帶着兩個標志位的 (rws rws rwx),經過末尾寫入一 byte 后兩個標志位消失了 (rwx rwx rwx),重新設置標志位並截斷文件后 (從 9.5 K 截短到 8K),兩個標志位也能消失,驗證了上面的結論。

后記

寫了這么多,是否已經把 linux 文件權限說盡了?非也非也。這都是幾十年前的東西了,現代 linux 也推出了更靈活的基於 ACL (Access Control List) 的訪問權限設置,可以針對某個用戶做單獨的設置,讓他可以或不能訪問某個特定目錄或文件,這比把用戶加入一個組並獲得該組所有目錄的訪問權限要安全的多。下面是與 ACL 相關的命令:

  • setfacl
  • getfacl

當設置了 acl 后,ls 的輸出也會有所不同,通常是在權限位末尾多一些特殊字符用以標記。此外還可以像 windows 一樣設置文件的屬性:

  • chattr
  • lsattr

選項的不同,對文件產生的影響也不同,這里羅列一些比較常用的選項:

  • i:只讀屬性
    • 普通文件:不允許對文件進行刪除、重命名、添加數據;
    • 目錄:不能創建、刪除、重命名目錄下的文件;
  • a:追加屬性
    • 普通文件:只能向文件中追加數據,不能刪除或編輯文件;
    • 目錄:只能在目錄中建立或修改文件,不能刪除文件;
  • ……

可以實現對文件、目錄行為更精細的控制。具體細節沒有做深入研究,這里只是作為一個引子,推薦各位讀者繼續探索。不過再高深的權限控制,也是以基礎知識作為根底的,例如設置 acl 時指定的權限位 rwx 就和我們在前面說明的完全一致。

行文至此,我主要想寫一個關於目錄權限的“突發奇想”——如果我只放開目錄的 wx 權限,不放開 r 權限,那么其它用戶能在這個目錄下做什么呢?根據前面的知識,我們知道 ’-wx‘ 權限下用戶可以創建、刪除、重命名文件,也可以通過目錄訪問其中的文件,唯獨不能列出目錄內容。那么這個目錄對於用戶就像是一個“黑暗森林”,誰也看不到別人,甚至看不到自己,呃……好像還是蠻有用的,因為好多安全問題就是你的文件暴露在了陌生人面前,如果他都看不到的話,你的文件是不是就更安全了呢?

如果再加入 svtx 位,一個用戶不能刪除、重命名另一個用戶的文件,這樣就更有意思了:文件的創建完全憑運氣,先到先占用,創建的文件失敗了,說明已經有人占用了這個名稱,你只能換個別的名稱再試。好在有子目錄,如果將所有工作都放在子目錄中進行,沖突的概率應該會大大降低。唯一不方便的是時間長了可能忘記自己創建過哪些文件,所以可能需要將創建過的文件記錄在一個清單中……

對於 root 或 owner 這個目錄則是一覽無余,擁有上帝視角,很好奇這樣一個目錄時間長了會發展成什么樣子……哈哈,以上只是一些不着邊際的想法,供大家一樂。

下載

本文相關的腳本都上傳在了 git 上,可以通過以下路徑訪問:

https://github.com/goodpaperman/apue/tree/master/04.chapter/permission

或者直接復刻整個庫到本地,再切換到對應目錄即可:

git clone git@github.com:goodpaperman/apue.git
cd apue/04.chapter/permission

每個用例對應一個或多個腳本,多個腳本時以數字后綴區分。在框架腳本 user_init.sh 中可以通過將條件語句修改為 true 來打開對應的用例,例如:

1 if true; then 
2     # case: write clear setuid 
3     cp ./setugid /tmp/
4     cp ./write_clear_setuid.sh /tmp/
5     su - lippman -s /tmp/write_clear_setuid.sh
6 fi

你可以打開所有的用例開關,不過那樣輸出就會混在一起,閱讀起來不是特別方便。

參考

[1]. Linux查看用戶所屬用戶組

[2]. 一個用戶最多能加入多少個組?

[3]. Linux的chmod與symbolic link

[4]. 文件的uid、gid 進程的euid 、egid 、附加組ID(如果支持) 總結

[5]. Linux SetGID(SGID)文件特殊權限用法詳解

[6]. Linux下查看某個用戶組下的所有用戶

[7]. Linux修改用戶所屬組的方法

[8]. shell不能執行su 后的腳本

[9]. shell腳本中使用其他用戶執行腳本

[10]. Linux, sudo with no password (免密碼sudo)

[11]. Linux命令:修改文件權限命令chmod、chgrp、chown詳解

[12]. 關於 Linux系統用戶、組和權限管理

[13]. Linux用戶(user)與用戶組(group)管理(超詳細解釋)

[14]. 配置 Linux 的訪問控制列表(ACL)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM