是誰在問,又想做什麼?
在這整個階段裡,你已經堆疊出檔案「是什麼」的一切:一個帶有屬性的抽象,透過描述符開啟而進入開啟檔案表,藉由穿越目錄的路徑被找到,並由硬連結與符號連結命名——也許不只一次。上述每一個操作,都悄悄假設了你被允許這麼做。這篇談的就是那個假設。保護是檔案系統對每一次存取都會問的那個問題所給的答案:這位特定的提問者,想對這個特定的檔案執行這個特定的動作——是或否?
請注意那個問題裡的三個部件,因為每一種存取控制方案都只是把對它的答案編碼起來的辦法:一個主體(誰在問——一位使用者,或更精確地說,是代表他行事的行程)、一個客體(想對什麼做——這裡指檔案或目錄),以及一個操作(讀、寫、執行、刪除)。核心就是那位在每一道門口查驗你識別證的大樓管理員。你遞出請求;它查閱記錄;它開門或拒絕。關鍵在於:這道查驗住在核心裡,位於你在基礎階段認識的使用者/核心界線中那個有特權的一側——使用者程式不能就這麼決定跳過它,因為這個判定是由使用者程式碰不到的程式碼做出的。
全景圖:存取矩陣
在 rwx 位元說得通之前,先看看它們所逼近的那個理想會很有幫助。想像一張巨大的表——存取矩陣——每個主體一列、每個客體一欄,而每一格裡放著那個主體能對那個客體執行的精確操作集合。「alice」這列、「/home/alice/diary.txt」這欄,格子寫著「讀、寫」。「bob」這列、同一欄,格子是空的:bob 什麼都不能做。這張表就是「誰能做什麼」完整而精確的真相。每一個真實的保護系統,都是某種儲存這張表的辦法——而不必真的蓋出一個龐大到讓檔案本身相形見絀的東西。
這張矩陣大部分是空的(多數主體幾乎碰不到任何客體),所以沒人會把它整張存下來。有兩種自然的切法。按欄切,你就把「允許碰它的主體清單」和每個客體存在一起——這正是存取控制清單(ACL),也是多數檔案系統的做法。按列切,你就給每個主體一份「它可以碰的客體清單」,像一串鑰匙圈——這就是能力清單,是檔案描述符與雲端權杖式系統背後的模型。而我們將會看到,熟悉的 rwx 權限位元是被壓縮過的第三種選項:一份被擠壓到只占幾個位元的 ACL。
the access matrix (ideal, mostly empty)
diary.txt report.doc /bin/ls
alice rw rw x
bob -- r x
carol -- -- x
by COLUMN -> store with each FILE the list of who may touch it = ACL
by ROW -> give each USER a list of files they may touch = capabilitiesrwx 位元:一個巧妙的九位元速記
Unix 的設計者下了一個節儉的賭注:與其為每個檔案存一份完整的使用者清單,不如只為三類主體存下答案。每個檔案都記錄它的擁有者(單一使用者)與一個擁有它的群組(一組具名的使用者),接著帶著三組、每組三個的權限位元——讀、寫、執行——一組給擁有者、一組給群組、一組給其他所有人(「其他」)。總共九個位元,而你已經見過它們住在哪裡:就在檔案的 inode 裡頭,與大小和時間戳並列,正是本階段第一篇提過的檔案屬性之一。這些位元跟著 inode 走、而不跟著任何名字走,所以一個檔案的所有硬連結共享同一組權限——因為只有一個 inode。
你把這九個位元由左到右讀成三組:擁有者、群組、其他。字串 rwxr-xr-x 表示擁有者可讀、可寫、可執行;群組可讀、可執行但不可寫;其他所有人同樣可讀、可執行但不可寫。一個橫線代表缺少該權限。同樣這九個位元常被寫成一個三位數的八進位數字,因為每一組剛好是三個位元:r 是 4、w 是 2、x 是 1,每組各自相加。於是 rwx 是 4+2+1 = 7、r-x 是 4+0+1 = 5,整串就是 755——這就是為什麼「chmod 755」和「rwxr-xr-x」說的是一模一樣的事。
核心實際上如何查驗,是個值得弄對的細節,因為它常讓人意外。它不會把三組合併。它挑出最具體的那一組,並只用那一組。如果你是檔案的擁有者,核心就查擁有者位元、然後停下——即使群組或其他位元更寬鬆。如果你不是擁有者、但身在檔案的群組裡,它就查群組位元、然後停下。否則查其他。一個真實的後果是:用 ---r--r-- 這種權限,擁有者可能被鎖在自己的檔案外面、而陌生人卻讀得到它,因為擁有者最先被查、卻發現自己沒有任何權利。最具體的那一桶勝出,不論它寬鬆與否。
讀、寫、執行——以及目錄這個陷阱
對一個普通檔案,這三個權限的意思和你猜的一樣。讀讓你看見那些位元組;寫讓你更改它們;執行讓你把這個檔案當程式來跑。注意:作業系統不會從檔案的名字或內容去推斷「這東西可以執行」——它去問執行位元。這正是檔案類型那一篇所強調的同一種分離:名稱的副檔名是個提示,但核心信任的是中繼資料。沒有設定執行位元,一支完全合法的程式就是跑不起來;而在一個資料檔上設了它,核心就會盡責地試著去執行一堆胡言亂語。
目錄是初學者最常被咬到的地方,因為同樣三個字母在那裡的意思不同。回想目錄那一篇:目錄其實是一個檔案,它的內容是一份把名字對映到 inode 編號的清單。所以:對目錄的讀讓你列出裡頭的名字。對目錄的寫讓你更改那份清單——建立、改名或刪除項目——這就是為什麼只要你能寫那個指名某檔案的目錄,你就能刪掉一個你甚至無權寫入的檔案。而目錄的執行和「跑」任何東西都無關;它的意思是「搜尋」——穿越通過這個目錄、去抵達其下所命名之物的權限。這正是路徑穿越在每一步實際查驗的那個位元。
當三個分類不夠時:ACL
這套九位元方案精簡得令人讚嘆,但它最多只能表達三個答案:一個給擁有者、一個給某一個群組、一個給其他所有人。現實生活卻動不動就需要更多。假設 alice 擁有一份報告,希望 bob 能讀也能寫、carol 只能讀、「finance」這個群組能讀,而其他任何人完全看不到它。沒有任何一種擁有者/群組/其他位元的指派能捕捉這個情境——bob 和 carol 需要不同的權利,但「其他」只給每個人同一個答案。位元的表達力用盡了。這正是存取控制清單證明自己價值的時刻。
ACL 放棄了三分類的折衷,改為在檔案上附掛一份明確的項目清單——每個項目指名一位特定的使用者或群組,以及他們得到的精確權限。它就是存取矩陣中那個「每檔案一欄」被化為現實的樣子:「bob: rw」「carol: r」「finance 群組: r」,再加一個給其他所有人的預設值。如今每個主體都能得到量身訂做的答案。順帶一提,經典的 rwx 位元並沒有消失;在現代系統上,它們以一個精簡的特例之姿存留下來,而 ACL 是當三個分類說不出你的意思時你會伸手去拿的那個通用形式。Windows 預設倚重 ACL;Linux 與 macOS 則把 rwx 位元留作日常情況,需要時再在其上疊加 ACL。
ACL 嚴格來說表達力更強,那為什麼不到處都用它?誠實的取捨是成本與清晰度。一份 ACL 比九個位元占更多空間、查起來也更慢,但更深的代價在於人:一個帶著二十個 ACL 項目的檔案,遠比 rwxr-x--- 難以推敲,而一個你不容易看見的權限,就是一個悄悄授出你早已忘記之存取的權限。這呼應了一條你現在應該對它已不陌生的、來自保護階段的原則——最小權限原則:只給每個主體它所需的最少權利、不多給。位元與 ACL 都服務於這個目標;位元讓它保持簡單,ACL 讓它保持精確,而一位深思的管理員會伸手去拿那個能讓實際政策最易於陳述也最易於稽核的工具。
誠實的界線:權限沒有承諾的事
一旦你的檔案有了整齊的權限,便很容易感到安全,所以讓我們精確地說清楚它們究竟給了你什麼、又沒給你什麼。首先,幾乎總有一個主體完全不受這些位元約束:超級使用者(Unix 上的 root)。root 會被拿去和權限位元比對,而依設計,它樣樣都通過——那位握有萬能鑰匙的大樓管理員。權限位元保護使用者免於彼此侵犯;它們不保護任何人免於 root,也不保護一個檔案免於一個已經越過特權界線的核心層級攻擊者。回想保護域這個想法:root 不過是運作在一個幾乎包含全部權利的域裡。
其次,權限假設核心已經知道是誰在問,而它對核心如何得知這件事隻字未提。如果攻擊者偷走了你的密碼、或劫持了一個以你身分執行的行程,那道權限查驗會完美運作——並忠實地把你被允許的一切都授予攻擊者。把身分認對是另一個獨立的問題(驗證),而把一個低權限的立足點變成高權限的,則自成一類攻擊(最小權限之所以存在,正是為了在那種事發生時縮小損害)。權限回答的是「這個主體可以嗎?」;它信任別人已經正確地回答了「這個主體是誰?」。
- 一個行程發出請求,例如 open("/a/b/c.txt", read)。函式庫把它包裝成一個系統呼叫並陷入核心——那是通往權限判定的唯一路徑。
- 核心解析路徑,沿途逐一查驗每個目錄的執行(搜尋)權限:/、然後 /a、然後 /a/b。任何一步失敗,穿越就以拒絕告終。
- 抵達 c.txt 後,核心讀取它的 inode,以找出擁有者、群組與(rwx)位元或 ACL——存著答案的那份中繼資料。
- 它拿提問的主體和那份中繼資料比對,挑出最具體的匹配(擁有者,否則群組,否則其他;或匹配的 ACL 項目),只授予那些權利。
- 若所請求的操作獲准,呼叫便繼續進行並回傳一個描述符;否則回傳一個「權限遭拒」的錯誤,而檔案絲毫不被觸碰。
那趟穿越為整個階段收尾:一條穿越目錄解析出的路徑(第三篇)、一個或許經由硬連結抵達的 inode(第四篇)、依其位元或 ACL 查驗的權限,最後是一個被交還進開啟檔案表的描述符(第二篇)——到了這一刻,這個檔案(第一篇)就任你讀寫了。權限是這整套機器必須通過的那道閘門,而現在你能精確看見:你學到的每一個部件,是如何拼合起來去回答那個小小的、不變的問題——是,還是否?