通用面試:Unix 網域套接字如何傳遞檔案描述符?
題幹與適用場景
兩個本地程序透過 Unix 網域套接字協作:特權程序開啟檔案或建立監聽 socket,再交給低權限 worker 使用。請解釋如何傳遞檔案描述符、接收端得到什麼,以及如何處理截斷和權限風險。
這道題考察 Linux/Unix IPC、檔案描述符表與核心 open file description 的區別,以及 sendmsg/recvmsg 的輔助資料協定。Linux unix(7) 將 SCM_RIGHTS 定義為在程序間傳送或接收一組已開啟檔案描述符。
面試官考察點
- 是否區分程序私有的 fd 整數與核心中的 open file description。
- 是否知道
SCM_RIGHTS透過 ancillary data 傳輸,而不是把整數寫入普通 payload。 - 是否能正確配置
cmsghdr、CMSGSPACE、CMSGLEN並檢查MSG_CTRUNC。 - 是否涵蓋 socket 路徑權限、傳送方身分、資源上限、關閉時機和失敗清理。
回答前需要釐清的問題
- 程序是否同一使用者、是否存在特權降級和明確的信任邊界?
- 需要傳遞的是普通檔案、已連線 socket、監聽 socket 還是 epoll/驅動 fd?
- 使用
SOCKSTREAM還是SOCKDGRAM?是否有訊息邊界和確認協定? - 接收端能否驗證傳送方憑據、資源類型、唯讀屬性和預期的 fd 數量?
30 秒回答框架
我會建立一對 Unix 網域套接字,用 sendmsg 的 SOLSOCKET/SCMRIGHTS 控制訊息攜帶 fd,並在普通資料中放協定版本和請求 ID。接收端用足夠大的 CMSGSPACE 緩衝區呼叫 recvmsg,檢查 cmsglevel、cmsgtype、長度和 MSGCTRUNC,然後把收到的 fd 當作本程序的新整數使用。傳遞的是對 open file description 的參照,接收端的 fd 數字通常不同。協定還要驗證對端憑據、限制數量、設定 close-on-exec,並在異常路徑關閉未接收或未使用的 fd。
分步驟深入解答
區分 fd 數字與 open file description
fd 是程序 fd table 中的整數索引;open file description 由核心保存開啟狀態,例如檔案偏移和狀態旗標。SCM_RIGHTS 複製的是對後者的參照,接收程序通常得到不同的 fd 整數,語義類似把 fd dup 到另一個程序的 fd table。
用 ancillary data 傳輸而非普通位元組
sendmsg 和 recvmsg 透過 msghdr.msgcontrol 攜帶 cmsghdr 鏈。cmsglevel 使用 SOLSOCKET,cmsgtype 使用 SCM_RIGHTS,資料區是整數 fd 陣列。普通 payload 可以攜帶版本、用途和確認 ID,但不能取代控制訊息。
正確計算控制緩衝區
傳送端為實際 fd 數量使用 CMSGLEN(n sizeof(int)) 設定 cmsglen;接收端至少分配 CMSGSPACE(n sizeof(int)) 的對齊空間。解析時遍歷 CMSGFIRSTHDR 和 CMSG_NXTHDR,並拒絕長度不足或類型不匹配的訊息。
處理截斷和串流邊界
接收控制緩衝區不足會導致輔助資料被截斷或丟棄,並設定 MSGCTRUNC;此時不能繼續使用部分 fd。Linux 對 SOCKSTREAM 要求攜帶至少一個真實位元組,且輔助資料形成接收屏障,因此協定需要把控制訊息與請求 ID 綁定,而不能只依賴位元組串流位置。
建立身分與權限邊界
檔案系統 socket 路徑的目錄和 socket 權限是第一層邊界。服務端還應使用 SOPEERCRED 或 SCMCREDENTIALS 驗證對端,並在應用層確認租戶、用途和資源類型。收到 fd 不代表取得更高權限,傳送方必須只傳遞經過授權的參照。
管理生命週期與資源上限
傳送後傳送程序可以關閉自己的 fd,但在接收方成功取出前,核心仍維護 in-flight 參照。Linux 會受 RLIMITNOFILE 和 SCMMAXFD 約束;目前 man page 記錄 SCMMAX_FD 通常為 253,舊版本曾為 255。服務應限制單條訊息和每個 worker 的 fd 數,並在超限時可觀測地拒絕。
struct msghdr msg = {0};
struct iovec iov = {.iov_base = "F", .iov_len = 1};
union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } control;
msg.msg_iov = &iov; msg.msg_iovlen = 1;
msg.msg_control = control.buf; msg.msg_controllen = sizeof(control.buf);
struct cmsghdr *c = CMSG_FIRSTHDR(&msg);
c->cmsg_level = SOL_SOCKET; c->cmsg_type = SCM_RIGHTS;
c->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(c), &fd, sizeof(fd));
sendmsg(sock, &msg, MSG_NOSIGNAL);高品質示範回答
我會把它設計成帶確認的本地 IPC 協定。傳送端用 Unix 網域 socket 的 sendmsg 攜帶 SOLSOCKET/SCMRIGHTS,普通資料放協定版本、用途和請求 ID;接收端用 CMSGSPACE 分配控制緩衝,檢查類型、長度和 MSGCTRUNC,驗證對端憑據、fd 數量和資源類型後再交給 worker。關鍵語義是傳遞 open file description 的參照,所以接收端的整數通常不同,檔案偏移等開啟狀態可能共享。系統還要設定 close-on-exec、限制 in-flight fd、處理 RLIMITNOFILE 和 SCMMAX_FD,並對路徑權限、失敗清理和稽核事件做閉環。
常見錯誤
- 把 fd 整數寫進普通 JSON 或位元組 payload,誤以為接收程序能直接使用它。
- 忽略
CMSG_SPACE對齊,只分配sizeof(int)的控制緩衝區。 - 不檢查
MSG_CTRUNC,繼續使用被截斷的 fd 清單。 - 只檢查 Unix socket 路徑,不驗證對端憑據和傳送用途。
- 忘記接收方得到的是新 fd 數字,也沒有約定共享偏移和狀態旗標的語義。
- 忽略 close-on-exec、fd 上限、傳送失敗和未使用 fd 的關閉路徑。
追問及應對
接收端拿到的是同一個檔案描述符嗎?
整數通常不同。核心把同一個 open file description 的參照複製到接收程序的 fd table,因此檔案偏移和部分開啟狀態可能共享;題目若要求獨立偏移,應重新開啟或複製資料,而不是假設 fd 數字相同。
為什麼需要傳送一個真實位元組?
Linux 的 Unix stream socket 在傳遞輔助資料時要求同一 sendmsg 包含至少一個真實位元組;它也幫助協定把控制訊息與請求關聯。datagram 在 Linux 上可以不帶,但為了可攜性仍應傳送一個真實位元組。
如果控制緩衝區太小會怎樣?
輔助資料可能被截斷或丟棄,並設定 MSG_CTRUNC;接收端必須關閉已取出的無效或多餘 fd,返回協定錯誤並記錄事件,不能把部分清單當作完整授權。
如何防止特權程序誤傳資源?
先用 socket 檔案權限和對端憑據限制連線,再在應用層綁定請求 ID、租戶、用途和資源類型。傳送端只從允許清單選擇 fd,接收端驗證唯讀屬性、路徑或 socket 狀態,並對每次授權和關閉記錄稽核。