GUI 虛擬化

qubes-guiqubes-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 有兩個原因:

  1. xc_map_foreign_pages 只能由 dom0 使用,因為它可以存取任何 VM 的所有記憶體。允許 dom0 以外的任何 VM 執行此操作將構成安全漏洞。

  2. xc_map_foreign_pages 需要待映射頁面的客戶機實體位址,但普通的使用者空間進程(如 qubes-gui 或 Xorg)無法存取該資訊。因此,轉換是透過 u2mfn 這個樹外核心模組完成的。

目前,使用 WINDOW_DUMP 確實會帶來效能成本,因為 AppVM 的 X 伺服器必須將頁面從應用程式複製到 gntalloc 配置的記憶體中。這將可透過未來對 gntalloc 的改進來解決,使其能夠透過 gntalloc 匯出*任何*頁面,包括另一個進程共享的記憶體。

gui.png

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-guidxside.h 標頭檔的 clipboard_metadata 結構中找到。雖然 qubes-guid 的輸出完全符合 JSON 格式,但其解析器有限制。它期望每個鍵值對之後有換行符號,且每行只有一個鍵值對。開頭和結尾的花括號應各自獨立成行。不應有前導空白字元。

這樣一來,使用者可以快速地在 AppVM 之間複製剪貼簿。此操作完全由使用者控制,任何 AppVM 都無法觸發或強制執行。

qubes-guiqubes-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 訊息

正確處理以下訊息對安全性至關重要。請注意,除 CLIPBOARDMFNDUMPWINDOW_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」參數僅是佔位符(將由 qubes-guid 填寫),以便在與 shmoverride.so 通訊時可以使用相同的結構。

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 (0)。

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;
};

告訴 qubes_drv 驅動程式產生按鍵事件

MSG_BUTTON

struct msg_button {
  uint32_t type;
  uint32_t x;
  uint32_t y;
  uint32_t state;
  uint32_t button;
};

告訴 qubes_drv 驅動程式產生滑鼠點擊事件

MSG_MOTION

struct msg_motion {
  uint32_t x;
  uint32_t y;
  uint32_t state;
  uint32_t is_hint;
};

告訴 qubes_drv 驅動程式產生移動事件

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;
};

視窗狀態變更確認

KEYPRESSBUTTONMOTIONFOCUS 訊息傳遞從 dom0 XEvent 中提取的資訊;請參閱相應的事件文件。