GUI 虛擬化
qubes-gui 與 qubes-guid 進程
所有 AppVM 的 X 應用程式會連接到在本機(AppVM 內執行)的 Xorg 伺服器,這些伺服器使用下列「硬體」驅動程式:
dummyqsb_drv- 視訊驅動程式,會繪製到位於 RAM 中的影格緩衝區,不連接實際硬體qubes_drv- 提供虛擬鍵盤與滑鼠(實際上還有更多功能,詳見下文)
對於每個 AppVM,都有一對透過 vchan 連接的 qubes-gui``(在 AppVM 中執行)與 ``qubes-guid``(在 AppVM 的 GuiVM 中執行,預設為 dom0)進程。``qubes-gui 的主要職責如下:
在根視窗上呼叫 XCompositeRedirectSubwindows,使每個視窗擁有自己的合成緩衝區
指示本機 Xorg 伺服器通知其關於視窗建立、配置及損毀事件;並將這些事件的資訊傳遞給 dom0
從 dom0 接收鍵盤與滑鼠事件的資訊,告訴
qubes-drv模擬相應的事件接收關於視窗大小/位置變更的資訊,並將其套用至本機視窗
qubes-guid 的主要職責如下:
每當從
qubes-gui收到 AppVM 中建立視窗的資訊時,在 dom0 中建立一個視窗每當本機視窗收到 XEvent 時,將其資訊傳遞給 AppVM(特別是滑鼠與鍵盤資料)
每當 AppVM 發出損毀事件訊號時,告訴本機 Xorg 伺服器重新繪製指定的視窗片段
接收關於視窗大小/位置變更的資訊,並將其套用至本機視窗
請注意,只有在屬於此 AppVM 的視窗取得焦點時,鍵盤與滑鼠事件才會傳遞給該 AppVM。AppVM 無法獲取輸入至其他 AppVM 的按鍵資訊(例如 XTEST 擴充功能只會報告本機 AppVM 鍵盤的狀態),也無法合成事件並傳遞給其他 AppVM。
視窗內容更新實作
典型的遠端桌面應用程式(如 VNC)會透過帶內方式(例如透過 TCP)傳遞所有變更的視窗內容資訊。由於該通道的吞吐量有限,這會影響視訊效能。在 Qubes 中,qubes-gui 不會透過 vchan 傳輸所有變更的像素。相反地,對於每個視窗,在建立或調整大小時:
舊版的
qubes-gui會向qubes-drv驅動程式索取保存視窗合成緩衝區的實體記憶體頁框清單,並透過已棄用的MFNDUMP訊息將其傳遞給 dom0。新版的
qubes-gui則依賴qubes-drv使用 gntalloc 分配記憶體,然後將 gntalloc 所選的許可表索引透過WINDOW_DUMP訊息傳遞給 GUI 守護進程。
現在,qubes-guid 必須告知 dom0 的 Xorg 伺服器緩衝區的位置。目前沒有支援的方式(例如 Xorg 擴充功能)可以實現這種零複製風格。Qubes 使用以下方法:
在 dom0 中,Xorg 伺服器以
LD_PRELOAD方式載入名為shmoverride.so的函數庫啟動。此函數庫會掛接所有與共享記憶體相關的函數呼叫。qubes-guid建立一個共享記憶體區段,然後告訴 Xorg 透過MIT-SHM擴充功能附加該區段當 Xorg 嘗試附加該區段(透過 glibc 的
shmat)時,shmoverride.so會攔截此呼叫,並改為針對已棄用的MFNDUMP訊息透過xc_map_foreign_pages映射AppVM 記憶體,或針對WINDOW_DUMP訊息透過xengnttab_map_domain_grant_refs映射。之後,我們可以使用 MIT-SHM 函數(例如
XShmPutImage)繪製到 dom0 視窗上。XShmPutImage將以 DRAM 速度進行繪製,許多驅動程式還會使用 DMA 使其更快。
重要細節在於 xc_map_foreign_pages 會驗證給定的 mfn 範圍是否確實屬於某個網域 ID(而後者由受信任的 qubes-guid 提供)。因此,惡意 AppVM 無法透過在 MFNDUMP 訊息中傳遞精心偽造的 mfn 獲得任何好處。同樣地,xengnttab_map_domain_grant_refs 只會映射 qubes-guid 指定的特定網域 ID 的許可,因此偽造的 WINDOW_DUMP 訊息對攻擊者來說也無用處。
總結來說,此解決方案具有以下優點:
以 DRAM 速度更新視窗
無需修改 Xorg 程式碼
支援程式碼的體積最小化
WINDOW_DUMP 優於 MFNDUMP 有兩個原因:
xc_map_foreign_pages只能由 dom0 使用,因為它可以存取任何 VM 的所有記憶體。允許 dom0 以外的任何 VM 執行此操作將構成安全漏洞。xc_map_foreign_pages需要待映射頁面的客戶機實體位址,但普通的使用者空間進程(如qubes-gui或 Xorg)無法存取該資訊。因此,轉換是透過u2mfn這個樹外核心模組完成的。
目前,使用 WINDOW_DUMP 確實會帶來效能成本,因為 AppVM 的 X 伺服器必須將頁面從應用程式複製到 gntalloc 配置的記憶體中。這將可透過未來對 gntalloc 的改進來解決,使其能夠透過 gntalloc 匯出*任何*頁面,包括另一個進程共享的記憶體。
dom0 視窗上的安全標註
使用者知道某個視窗屬於哪個 AppVM 是非常重要的。這可以防止惡意 AppVM 繪製一個假裝屬於其他 AppVM 或 dom0 的視窗,並試圖竊取例如密碼等資訊。
在 Qubes 中,使用了一個自訂的視窗裝飾器,會在已裝飾的視窗周圍繪製彩色邊框(顏色在 AppVM 建立時決定)。此外,視窗標題始終以 [AppVM 名稱] 開頭。如果視窗具有 override_redirect 屬性(表示不應由視窗管理器處理,典型情況是選單視窗),qubes-guid 會手動在內部繪製一個兩像素的彩色邊框。
剪貼簿共享實作
當然,允許 AppVM 無條件讀取/寫入其他 AppVM 的剪貼簿是不安全的。因此,使用了以下機制:
dom0 中有一個「qubes 剪貼簿」- 其內容儲存在 dom0 的普通檔案中,路徑為
/run/qubes/qubes-clipboard.bin。如果使用者想將本機 AppVM 的剪貼簿複製到 qubes 剪貼簿,她必須將焦點放在屬於此 AppVM 的任何視窗上,然後按下 Ctrl-Shift-C。此組合鍵會被
qubes-guid攔截,並向 AppVM 發送CLIPBOARD_REQ訊息。qubes-gui則以CLIPBOARD_DATA訊息回應,後面跟著剪貼簿內容。使用者將焦點放在其他 AppVM 視窗上,按下 Ctrl-Shift-V。此組合鍵會被
qubes-guid攔截,並向 AppVM 發送CLIPBOARD_DATA訊息,後面跟著 qubes 剪貼簿的內容;qubes-gui將資料複製到本機剪貼簿,然後使用者就可以正常地將其內容貼上到本機應用程式中。在全域剪貼簿複製或貼上操作時,會將補充的 JSON 後設資料檔案儲存為
/run/qubes/qubes-clipboard.bin.metadata。每個欄位的說明可在qubes-guid的xside.h標頭檔的clipboard_metadata結構中找到。雖然qubes-guid的輸出完全符合 JSON 格式,但其解析器有限制。它期望每個鍵值對之後有換行符號,且每行只有一個鍵值對。開頭和結尾的花括號應各自獨立成行。不應有前導空白字元。
這樣一來,使用者可以快速地在 AppVM 之間複製剪貼簿。此操作完全由使用者控制,任何 AppVM 都無法觸發或強制執行。
qubes-gui 與 qubes-guid 程式碼說明
這兩個應用程式的結構類似。它們使用 select 函數來等待以下兩個事件來源之一:
來自本機 X 伺服器的訊息
來自連接到遠端端的 vchan 訊息
XEvents 由 handle_xevent_eventname 函數處理,而訊息則由 handle_messagename 函數處理。在修改實際的 select 迴圈時應非常小心,因為 XEvents 和 vchan 訊息都會被緩衝,select 不會為每個訊息都喚醒。
如果變更了訊息的數量/順序/簽名,則應增加 messages.h 包含檔案中的 QUBES_GUID_PROTOCOL_VERSION 常數。
qubes-guid 將除錯資訊寫入 /var/log/qubes/qubes.domain_id.log 檔案;qubes-gui 將除錯資訊寫入 /var/log/qubes/gui_agent.log 檔案。回報錯誤時請附上這些檔案。
AppVM -> GuiVM 訊息
正確處理以下訊息對安全性至關重要。請注意,除 CLIPBOARD、MFNDUMP 和 WINDOW_DUMP 之外的所有訊息都具有固定大小,因此解析程式碼可以很精簡。
override_redirect 視窗屬性的說明請參閱覆蓋重新導向旗標。transient_for 屬性的說明請參閱 transient_for 屬性。
視窗管理器的提示與旗標在 延伸視窗管理器提示 (EWMH) 規範 中有說明,特別是在 _NET_WM_STATE 章節中。
每條訊息的開頭為以下標頭:
struct msghdr {
uint32_t type;
uint32_t window;
/* This field is intended for use by GUI agents to skip unknown
* messages from the (trusted) GUI daemon. GUI daemon, on the other
* hand, should never rely on this field to calculate the actual len
* of message to be read, as the (untrusted) agent can put whatever
* it wants here! */
uint32_t untrusted_len;
};
該標頭後面接著訊息特定的資料:
訊息名稱 |
標頭後的結構 |
動作 |
|---|---|---|
MSG_CLIPBOARD_DATA |
無定型二進位資料(在 1.2 之前的協定中,長度由「window」欄位決定;在 1.2 及之後則由標頭中的 untrusted_len 決定) |
儲存接收到的剪貼簿內容(不以任何方式解析) |
MSG_CREATE |
struct msg_create {
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
uint32_t parent;
uint32_t override_redirect;
};
|
使用給定的參數建立視窗 |
MSG_DESTROY |
無 |
銷毀視窗 |
MSG_MAP |
struct msg_map_info {
uint32_t transient_for;
uint32_t override_redirect;
};
|
使用給定的參數映射視窗 |
MSG_UNMAP |
無 |
取消映射視窗 |
MSG_CONFIGURE |
struct msg_configure {
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
uint32_t override_redirect;
};
|
變更視窗位置/大小/類型 |
MSG_MFNDUMP |
struct shm_cmd {
uint32_t shmid;
uint32_t width;
uint32_t height;
uint32_t bpp;
uint32_t off;
uint32_t num_mfn;
uint32_t domid;
uint32_t mfns[0];
};
|
擷取構成遠端視窗合成緩衝區的 mfn 陣列。 「num_mfn」個 32 位元整數跟在 shm_cmd 結構之後;「off」是合成緩衝區在第一個頁框中的起始偏移量;「shmid」和「domid」參數僅是佔位符(將由 |
MSG_SHMIMAGE |
struct msg_shmimage {
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
};
|
重新繪製給定的視窗片段 |
MSG_WMNAME |
struct msg_wmname {
char data[128];
};
|
設定視窗名稱。僅允許可列印字元,且預設不允許非 ASCII 字元。 |
MSG_DOCK |
無 |
將視窗停靠在系統匣中 |
MSG_WINDOW_HINTS |
struct msg_window_hints {
uint32_t flags;
uint32_t min_width;
uint32_t min_height;
uint32_t max_width;
uint32_t max_height;
uint32_t width_inc;
uint32_t height_inc;
uint32_t base_width;
uint32_t base_height;
};
|
視窗管理器的尺寸提示 |
MSG_WINDOW_FLAGS |
struct msg_window_flags {
uint32_t flags_set;
uint32_t flags_unset;
};
|
變更視窗狀態請求;欄位包含位元遮罩,標示哪些旗標請求設定、哪些取消設定 |
MSG_CURSOR |
struct msg_cursor {
uint32_t cursor;
};
|
更新視窗的游標指標。支援的游標 ID 為預設游標 (0) 和 `X 字型游標 <https://tronche.com/gui/x/xlib/appendix/b/>`__(設定 0x100 位元)。 |
MSG_WMCLASS |
struct msg_wmclass {
char res_class[64];
char res_name[64];
};
|
設定視窗的 WM_CLASS 屬性。 |
MSG_WINDOW_DUMP |
struct msg_window_dump_hdr {
uint32_t type;
uint32_t width;
uint32_t height;
uint32_t bpp;
};
|
類型為 hdr.type 的共享記憶體傾印命令的標頭。目前僅支援 |
WINDOW_DUMP_TYPE_GRANT_REFS |
struct msg_window_dump_grant_refs {
uint32_t refs[0];
};
|
應映射到合成緩衝區中的許可參考。 |
GuiVM -> AppVM 訊息
以下訊息的正確處理**不**涉及安全性關鍵。
每條訊息的開頭為以下標頭
struct msghdr {
uint32_t type;
uint32_t window;
};
該標頭後面接著訊息特定的資料:
訊息名稱 |
標頭後的結構 |
動作 |
|---|---|---|
MSG_KEYPRESS |
struct msg_keypress {
uint32_t type;
uint32_t x;
uint32_t y;
uint32_t state;
uint32_t keycode;
};
|
告訴 |
MSG_BUTTON |
struct msg_button {
uint32_t type;
uint32_t x;
uint32_t y;
uint32_t state;
uint32_t button;
};
|
告訴 |
MSG_MOTION |
struct msg_motion {
uint32_t x;
uint32_t y;
uint32_t state;
uint32_t is_hint;
};
|
告訴 |
MSG_CONFIGURE |
struct msg_configure {
uint32_t x;
uint32_t y;
uint32_t width;
uint32_t height;
uint32_t override_redirect;
};
|
變更視窗位置/大小/類型 |
MSG_MAP |
struct msg_map_info {
uint32_t transient_for;
uint32_t override_redirect;
};
|
使用給定的參數映射視窗 |
MSG_CLOSE |
無 |
向視窗發送 wmDeleteMessage |
MSG_CROSSING |
struct msg_crossing {
uint32_t type;
uint32_t x;
uint32_t y;
uint32_t state;
uint32_t mode;
uint32_t detail;
uint32_t focus;
};
|
通知視窗進入/離開事件 |
MSG_FOCUS |
struct msg_focus {
uint32_t type;
uint32_t mode;
uint32_t detail;
};
|
提起視窗,XSetInputFocus |
MSG_CLIPBOARD_REQ |
無 |
擷取本機剪貼簿,將內容傳遞給 gui 守護進程 |
MSG_CLIPBOARD_DATA |
無定型二進位資料 |
將接收到的資料插入本機剪貼簿 |
MSG_EXECUTE |
已過時 |
已過時,未使用 |
MSG_KEYMAP_NOTIFY |
unsigned char remote_keys[32]; |
與 dom0 同步鍵盤狀態(按鍵按下/釋放) |
MSG_WINDOW_FLAGS |
struct msg_window_flags {
uint32_t flags_set;
uint32_t flags_unset;
};
|
視窗狀態變更確認 |
KEYPRESS、BUTTON、MOTION、FOCUS 訊息傳遞從 dom0 XEvent 中提取的資訊;請參閱相應的事件文件。