מערכת Android מספקת הטמעה לדוגמה של כל הרכיבים שנדרשים להטמעה של Android Virtualization Framework. בשלב הזה, ההטמעה הזו מוגבלת ל-ARM64. בדף הזה מוסברת ארכיטקטורת המסגרת.
רקע
ארכיטקטורת Arm מאפשרת עד ארבע רמות חריגה, כאשר רמת חריגה 0 (EL0) היא הרמה עם הכי פחות הרשאות, ורמת חריגה 3 (EL3) היא הרמה עם הכי הרבה הרשאות. החלק הגדול ביותר של ה-codebase של Android (כל הרכיבים של סביבת המשתמש) פועל ב-EL0. שאר המערכת, שלרוב נקראת 'Android', היא ליבת לינוקס שפועלת ברמת EL1.
שכבת EL2 מאפשרת להציג היפרויזור שמאפשר לבודד זיכרון ומכשירים למכונות pVM נפרדות ברמות EL1/EL0, עם הבטחות חזקות של סודיות ויושרה.
Hypervisor
המכונה הווירטואלית המוגנת שמבוססת על ליבת מערכת (pKVM) מבוססת על ההיפר-ויז'ור Linux KVM, שנוספה לו היכולת להגביל את הגישה למטענים הייעודיים (payloads) שפועלים במכונות וירטואליות אורחות שמסומנות כ'מוגנות' בזמן היצירה.
KVM/arm64 תומך במצבי ביצוע שונים בהתאם לזמינות של תכונות מסוימות במעבד, כלומר, Virtualization Host Extensions (VHE) (ARMv8.1 ואילך). באחד מהמצבים האלה, שנקרא בדרך כלל מצב לא-VHE, קוד ההיפר-ויז'ר מפוצל מתמונת הליבה במהלך האתחול ומוגדר ב-EL2, בעוד שהליבה עצמה פועלת ב-EL1. למרות שרכיב EL2 של KVM הוא חלק מבסיס הקוד של Linux, הוא רכיב קטן שאחראי על המעבר בין כמה רכיבי EL1. רכיב ההיפר-ויז'ור עובר קומפילציה עם Linux, אבל הוא נמצא בקטע זיכרון נפרד וייעודי של תמונת vmlinux. pKVM משתמש בעיצוב הזה כדי להרחיב את קוד ההיפר-ויז'ור עם תכונות חדשות שמאפשרות לו להטיל הגבלות על ליבת המארח של Android ועל מרחב המשתמש, ולהגביל את גישת המארח לזיכרון האורח ולהיפר-ויז'ור.
מודולים של ספק pKVM
מודול ספק pKVM הוא מודול ספציפי לחומרה שמכיל פונקציונליות ספציפית למכשיר, כמו מנהלי התקנים של יחידת ניהול זיכרון קלט-פלט (IOMMU). המודולים האלה מאפשרים להעביר תכונות אבטחה שדורשות גישה ברמת חריגה 2 (EL2) ל-pKVM.
כדי ללמוד איך להטמיע ולטעון מודול ספק pKVM, אפשר לעיין במאמר בנושא הטמעה של מודול ספק pKVM.
תהליך ההפעלה
באיור הבא מוצג תהליך האתחול של pKVM:
- אתחול: תוכנת האתחול נכנסת לליבת הגנרית ב-EL2. קוד ליבת מהימן ברמות EL2 ו-EL1 מאתחל את pKVM ואת המודולים שלו. במהלך השלב הזה, רמת ההרשאות EL1 מהימנה על ידי רמת ההרשאות EL2, כך שלא מבוצע קוד לא מהימן.
- הסרת הרשאות מליבה: הליבה הגנרית מזהה שהיא פועלת ב-EL2 ומסירה את ההרשאות שלה ל-EL1. pKVM והמודולים שלה ממשיכים לפעול ב-EL2.
- Runtime: ליבת המערכת הגנרית ממשיכה את האתחול כרגיל, וטוענת את כל מנהלי ההתקנים הדרושים עד שהיא מגיעה למרחב המשתמש. בשלב הזה, pKVM כבר פועל ומטפל בטבלאות הדפים של שלב 2.
תהליך האתחול מסתמך על תוכנת האתחול כדי לאמת ולשמור על תקינות קובץ האימג' של הליבה בשלב האתחול. אחרי שהליבה מאבדת את ההרשאות שלה, היא כבר לא נחשבת מהימנה על ידי ההיפר-ויז'ור, שאחראי להגן על עצמו גם אם הליבה נפגעה.
הליבה של Android וההיפר-ויזור נמצאים באותו קובץ אימג' בינארי, ולכן יש ביניהם ממשק תקשורת צמוד מאוד. השילוב ההדוק הזה מבטיח עדכונים אטומיים של שני הרכיבים, כך שלא צריך לשמור על יציבות הממשק ביניהם, ומציע גמישות רבה בלי לפגוע ביכולת התחזוקה לטווח ארוך. הצימוד החזק מאפשר גם לבצע אופטימיזציה של הביצועים כששני הרכיבים יכולים לפעול יחד בלי לפגוע בהבטחות האבטחה שמספק ההיפר-ויזור.
בנוסף, הטמעה של GKI במערכת האקולוגית של Android מאפשרת באופן אוטומטי לפרוס את ההיפר-ויז'ר pKVM במכשירי Android באותו קובץ בינארי כמו הליבה.
הגנה על גישה לזיכרון המעבד
ארכיטקטורת Arm מציינת יחידה לניהול זיכרון (MMU) שמפוצלת לשני שלבים עצמאיים, שכל אחד מהם יכול לשמש להטמעה של תרגום כתובות ובקרת גישה לחלקים שונים בזיכרון. יחידת ניהול הזיכרון (MMU) בשלב 1 נשלטת על ידי EL1 ומאפשרת רמה ראשונה של תרגום כתובות. מערכת Linux משתמשת ב-MMU בשלב 1 כדי לנהל את מרחב הכתובות הווירטואלי שמוקצה לכל תהליך במרחב המשתמשים ולמרחב הכתובות הווירטואלי שלה.
יחידת ניהול הזיכרון (MMU) בשלב 2 נשלטת על ידי EL2 ומאפשרת להחיל תרגום כתובות שני על כתובת הפלט של יחידת ניהול הזיכרון (MMU) בשלב 1, וכך מתקבלת כתובת פיזית (PA). התרגום בשלב 2 יכול לשמש היפרויזורים כדי לשלוט בגישה לזיכרון ולתרגם אותה מכל המכונות הווירטואליות של האורחים. כפי שמוצג באיור 2, כששני שלבי התרגום מופעלים, כתובת הפלט של שלב 1 נקראת כתובת פיזית ביניים (IPA). הערה: הכתובת הווירטואלית (VA) מתורגמת לכתובת IPA ואז לכתובת PA.
בעבר, KVM פעל עם תרגום של שלב 2 כשהוא הפעיל אורחים, ועם תרגום של שלב 2 מושבת כשהוא הפעיל את ליבת Linux של המארח. הארכיטקטורה הזו מאפשרת גישה לזיכרון מ-MMU של שלב 1 במארח דרך MMU של שלב 2, ולכן מאפשרת גישה בלתי מוגבלת מהמארח לדפי הזיכרון של האורח. לעומת זאת, pKVM מאפשרת הגנה בשלב 2 גם בהקשר של המארח, ומטילה על ההיפר-ויזור את האחריות להגנה על דפי הזיכרון של האורח במקום על המארח.
KVM משתמש באופן מלא בתרגום כתובות בשלב 2 כדי להטמיע מיפויים מורכבים של IPA/PA לאורחים, וכך יוצר אשליה של זיכרון רציף לאורחים למרות פיצול פיזי. עם זאת, השימוש ב-MMU בשלב 2 עבור המארח מוגבל רק לבקרת גישה. המארח בשלב 2 ממופה לזהות, כדי להבטיח שזיכרון רציף במרחב ה-IPA של המארח יהיה רציף במרחב ה-PA. הארכיטקטורה הזו מאפשרת שימוש במיפויים גדולים בטבלת הדפים, וכתוצאה מכך מצמצמת את העומס על מאגר הנתונים הזמני לחיפוש תרגום (TLB). מכיוון ש-PA יכול לאנדקס מיפוי זהויות, שלב 2 של המארח משמש גם למעקב אחרי הבעלות על הדף ישירות בטבלת הדפים.
הגנה על גישה ישירה לזיכרון (DMA)
כמו שצוין קודם, ביטול המיפוי של דפי אורחים ממארח Linux בטבלאות הדפים של ה-CPU הוא שלב הכרחי אבל לא מספיק להגנה על זיכרון האורח. בנוסף, pKVM צריך להגן מפני גישה לזיכרון שמתבצעת על ידי מכשירים עם יכולת DMA בשליטת ליבת המארח, ומפני אפשרות של התקפת DMA שמתחילה ממארח זדוני. כדי למנוע ממכשיר כזה לגשת לזיכרון של האורח, pKVM דורש יחידת ניהול זיכרון של קלט/פלט (IOMMU) לכל מכשיר עם יכולת DMA במערכת, כמו שמוצג באיור 3.
לפחות, חומרת IOMMU מספקת את האמצעים להענקת גישת קריאה/כתיבה למכשיר לזיכרון הפיזי ברמת הגרנולריות של הדף, ולביטול הגישה. עם זאת, חומרת ה-IOMMU הזו מגבילה את השימוש במכשירים ב-pVM, כי היא מניחה שמתבצע מיפוי זהויות בשלב 2.
כדי להבטיח בידוד בין מכונות וירטואליות, עסקאות בזיכרון שנוצרות בשם ישויות שונות צריכות להיות ניתנות להבחנה על ידי IOMMU, כדי שאפשר יהיה להשתמש בקבוצה המתאימה של טבלאות דפים לתרגום.
בנוסף, צמצום כמות הקוד הספציפי ל-SoC ב-EL2 הוא אסטרטגיה מרכזית לצמצום בסיס המחשוב המהימן (TCB) הכולל של pKVM, והוא מנוגד להכללה של מנהלי התקנים של IOMMU בהיפר-ויז'ור. כדי לצמצם את הבעיה הזו, המארח ברמה EL1 אחראי למשימות ניהול עזר של IOMMU, כמו ניהול צריכת חשמל, הפעלה וטיפול בהפסקות, במקרים המתאימים.
עם זאת, אם המארח שולט במצב המכשיר, יש דרישות נוספות לממשק התכנות של חומרת ה-IOMMU כדי לוודא שאי אפשר לעקוף את בדיקות ההרשאות באמצעים אחרים, למשל אחרי איפוס המכשיר.
יחידת ניהול זיכרון המערכת (SMMU) של Arm היא יחידת IOMMU סטנדרטית עם תמיכה טובה למכשירי Arm, שמאפשרת גם בידוד וגם הקצאה ישירה. הארכיטקטורה הזו היא פתרון ההפניה המומלץ.
הבעלות על הזיכרון
בזמן האתחול, כל הזיכרון שאינו זיכרון של hypervisor נחשב כזיכרון בבעלות המארח, וה-hypervisor עוקב אחריו ככזה. כשמפעילים pVM, המארח תורם דפי זיכרון כדי לאפשר את האתחול, וה-hypervisor מעביר את הבעלות על הדפים האלה מהמארח ל-pVM. לכן, ההיפר-ויזור מטמיע הגבלות של בקרת גישה בטבלת הדפים של שלב 2 במארח, כדי למנוע גישה חוזרת לדפים, וכך מספק סודיות לאורח.
התקשורת בין המארח לבין האורחים מתאפשרת באמצעות שיתוף מבוקר של הזיכרון ביניהם. לאורחים מותר לשתף חלק מהדפים שלהם עם המארח באמצעות hypercall, שמורה להיפר-ויזור למפות מחדש את הדפים האלה בטבלת הדפים של המארח בשלב 2. באופן דומה, התקשורת של המארח עם TrustZone מתאפשרת באמצעות שיתוף זיכרון ו/או פעולות השאלה, שכולן מפוקחות ונשלטות באופן הדוק על ידי pKVM באמצעות מפרט Firmware Framework for Arm (FF-A).
דרישות הזיכרון של pVM יכולות להשתנות לאורך זמן, ולכן יש hypercall שמאפשר להעביר בחזרה למארח את הבעלות על דפים ספציפיים ששייכים למי שקורא ל-hypercall. בפועל, קריאה ל-Hypervisor זו משמשת עם פרוטוקול ה-balloon של virtio כדי לאפשר ל-VMM לבקש זיכרון בחזרה מ-pVM, ול-pVM להודיע ל-VMM על דפים שהוקצו, בצורה מבוקרת.
ההיפרווייזר אחראי למעקב אחר הבעלות על כל דפי הזיכרון במערכת, ולמעקב אחר השיתוף שלהם עם ישויות אחרות או ההשאלה שלהם לישויות אחרות. רוב המעקב הזה אחר המצב מתבצע באמצעות מטא-נתונים שמצורפים לטבלאות הדפים של המארח והאורחים בשלב 2, באמצעות ביטים שמורים ברשומות של טבלאות הדפים (PTE), שכשמן כן הן, שמורות לשימוש בתוכנה.
המארח צריך לוודא שהוא לא מנסה לגשת לדפים שהגישה אליהם נחסמה על ידי ההיפר-ויז'ור. גישה לא חוקית למארח גורמת להיפר-ויז'ר להחדיר חריגה סינכרונית למארח, שיכולה לגרום למשימת מרחב המשתמש האחראית לקבל אות SEGV, או לקריסת ליבת המארח. כדי למנוע גישה לא מכוונת, ליבות המארח לא יכולות להחליף או למזג דפים שניתנו לאורחים.
טיפול בהפרעות וטיימרים
הפסקות הן חלק חיוני מהדרך שבה אורח מקיים אינטראקציה עם מכשירים ומהתקשורת בין מעבדים, שבה הפסקות בין מעבדים (IPI) הן מנגנון התקשורת העיקרי. מודל ה-KVM הוא להעביר את כל ניהול ההפסקות הווירטואליות למארח ב-EL1, שמתנהג למטרה הזו כחלק לא מהימן של ההיפר-ויזור.
pKVM מציע הדמיה מלאה של Generic Interrupt Controller גרסה 3 (GICv3) שמבוססת על קוד KVM קיים. הטיימרים וה-IPIs מטופלים כחלק מקוד האמולציה הלא מהימן הזה.
תמיכה ב-GICv3
הממשק בין EL1 ל-EL2 צריך להבטיח שמצב ההפרעה המלא יהיה גלוי למארח EL1, כולל עותקים של רישום ההיפר-ויזורים שקשורים להפרעות. הנראות הזו מושגת בדרך כלל באמצעות אזורי זיכרון משותפים, אחד לכל מעבד וירטואלי (vCPU).
אפשר לפשט את קוד התמיכה בזמן ריצה של רישום המערכת כך שיתמוך רק בלכידת רישום של Software Generated Interrupt Register (SGIR) ו-Deactivate Interrupt Register (DIR). הארכיטקטורה מחייבת שהרישומים האלה תמיד יופנו ל-EL2, בעוד שההפניות האחרות היו עד עכשיו שימושיות רק לצורך צמצום שגיאות. כל השאר מטופל בחומרה.
בצד של MMIO, כל הפעולות מבוצעות באמולציה ברמה EL1, תוך שימוש חוזר בכל התשתית הנוכחית ב-KVM. לבסוף, הפקודה Wait for Interrupt (WFI) מועברת תמיד ל-EL1, כי זו אחת מהפרימיטיבים הבסיסיים לתזמון שבהם נעשה שימוש ב-KVM.
תמיכה בטיימר
ערך ההשוואה של הטיימר הווירטואלי צריך להיות חשוף ל-EL1 בכל WFI של לכידה, כדי ש-EL1 יוכל להזריק פסיקות של טיימר בזמן שה-vCPU חסום. הטיימר הפיזי מודמה לחלוטין, וכל המלכודות מועברות ל-EL1.
טיפול ב-MMIO
כדי לתקשר עם מנהל המכונות הווירטואליות (VMM) ולבצע הדמיה של GIC, צריך להעביר את מלכודות ה-MMIO בחזרה למארח ב-EL1 לצורך מיון נוסף. pKVM דורש את הפעולות הבאות:
- כתובת ה-IP וגודל הגישה
- נתונים במקרה של כתיבה
- סדר הבתים של המעבד (CPU) בנקודת הלכידה
בנוסף, מלכודות עם רגיסטר למטרה כללית (GPR) כמקור או כיעד מועברות באמצעות רגיסטר וירטואלי מופשט.
ממשקי אורחים
אורח יכול לתקשר עם אורח מוגן באמצעות שילוב של hypercalls וגישה לזיכרון לאזורים שנתפסו. הגישה ל-Hypercalls מתבצעת בהתאם לתקן SMCCC, עם טווח ששמור להקצאה לספק על ידי KVM. ההיפר-קולים הבאים חשובים במיוחד לאורחים ב-pKVM.
קריאות כלליות ל-Hypervisor
- PSCI מספק מנגנון סטנדרטי לאורח לשליטה במחזור החיים של מעבדי ה-vCPU שלו, כולל הפעלה, השבתה וכיבוי המערכת.
- TRNG מספק מנגנון סטנדרטי לאורח כדי לבקש אנטרופיה מ-pKVM, שמעביר את הקריאה ל-EL3. המנגנון הזה שימושי במיוחד במקרים שבהם אי אפשר לסמוך על המארח שיבצע וירטואליזציה של מחולל מספרים אקראיים (RNG) בחומרה.
קריאות ל-Hypervisor של pKVM
- שיתוף הזיכרון עם המארח. בתחילה, המארח לא יכול לגשת לזיכרון של האורח, אבל גישה של המארח נדרשת לתקשורת בזיכרון משותף ולמכשירים וירטואליים למחצה שמסתמכים על מאגרי נתונים משותפים. הפונקציה Hypercalls לשיתוף דפים עם המארח ולביטול השיתוף שלהם מאפשרת למערכת האורחת להחליט בדיוק אילו חלקים בזיכרון יהיו נגישים לשאר מערכת Android, בלי שיהיה צורך בהעברת נתונים.
- העברת הזיכרון למארח. בדרך כלל, כל הזיכרון של האורח שייך לאורח עד שהוא נהרס. המצב הזה לא מתאים למכונות וירטואליות לטווח ארוך עם דרישות זיכרון שמשתנות לאורך זמן. ה-hypercall
relinquishמאפשר למכונה וירטואלית להעביר באופן מפורש את הבעלות על דפים בחזרה למארח בלי שיהיה צורך בסיום הפעולה של המכונה הווירטואלית. - הפעלת מלכודת לגישה לזיכרון במארח. באופן מסורתי, אם אורח KVM ניגש לכתובת שלא תואמת לאזור זיכרון תקין, השרשור של ה-vCPU יוצא למארח והגישה משמשת בדרך כלל ל-MMIO ומודמת על ידי ה-VMM במרחב המשתמש. כדי לאפשר את הטיפול הזה, pKVM צריך לפרסם פרטים על ההוראה שגרמה לשגיאה, כמו הכתובת שלה, פרמטרים של רישום ואולי התוכן שלהם, בחזרה למארח. פעולה כזו עלולה לחשוף בטעות נתונים רגישים מאורח מוגן אם המלכוד לא היה צפוי. pKVM פותר את הבעיה הזו על ידי התייחסות לשגיאות האלה כאל שגיאות קריטיות, אלא אם האורח הוציא בעבר קריאת מערכת (hypercall) כדי לזהות את טווח ה-IPA שגרם לשגיאה כטווח שניתן לגשת אליו כדי לחזור למארח. הפתרון הזה נקרא MMIO guard.
מכשיר קלט/פלט וירטואלי (virtio)
Virtio הוא תקן פופולרי, נייד ומוגמר להטמעה של מכשירים וירטואליים למחצה ולביצוע אינטראקציה איתם. רוב המכשירים שנחשפים לאורחים מוגנים מיושמים באמצעות virtio. בנוסף, Virtio הוא הבסיס להטמעה של vsock שמשמש לתקשורת בין אורח מוגן לבין שאר מערכת Android.
בדרך כלל, מכשירי Virtio מיושמים במרחב המשתמש של המארח על ידי VMM, שמיירט גישות לזיכרון שנלכדו מהאורח לממשק MMIO של מכשיר Virtio ומבצע אמולציה של ההתנהגות הצפויה. הגישה ל-MMIO יקרה יחסית כי כל גישה למכשיר דורשת הלוך ושוב ל-VMM וחזרה, ולכן רוב העברת הנתונים בפועל בין המכשיר לאורח מתבצעת באמצעות קבוצה של תורי virtqueue בזיכרון. הנחת יסוד מרכזית של virtio היא שהמארח יכול לגשת לזיכרון של האורח באופן שרירותי. ההנחה הזו ניכרת בעיצוב של התור הווירטואלי, שעשוי להכיל מצביעים למאגרי נתונים במכונה האורחת, שאמולציית המכשיר אמורה לגשת אליהם ישירות.
אפשר להשתמש בקריאות היתר (hypercalls) לשיתוף זיכרון שתוארו קודם כדי לשתף מאגרי נתונים של virtio מהמכונה האורחת למארחת, אבל השיתוף הזה מתבצע ברמת הגרנולריות של הדף, ויכול להיות שבסופו של דבר ייחשפו יותר נתונים מהנדרש אם גודל שטח האחסון הזמני קטן מגודל הדף. במקום זאת, המכונה האורחת מוגדרת להקצאה של תורי ה-virtqueue ושל מאגרי הנתונים התואמים שלה מחלון קבוע של זיכרון משותף, והנתונים מועתקים (מועברים) אל החלון וממנו לפי הצורך.
אינטראקציה עם TrustZone
אורחים לא יכולים ליצור אינטראקציה ישירה עם TrustZone, אבל המארח עדיין יכול להוציא קריאות SMC לעולם המאובטח. השיחות האלה יכולות לציין מאגרי זיכרון עם כתובות פיזיות שהמארח לא יכול לגשת אליהם. מכיוון שהתוכנה המאובטחת בדרך כלל לא מודעת לנגישות של המאגר, מארח זדוני יכול להשתמש במאגר הזה כדי לבצע מתקפת סגן מבולבל (בדומה למתקפת DMA). כדי למנוע מתקפות כאלה, pKVM לוכד את כל קריאות ה-SMC של המארח ל-EL2 ופועל כפרוקסי בין המארח לבין המוניטור המאובטח ב-EL3.
שיחות PSCI מהמארח מועברות לקושחה EL3 עם שינויים מינימליים. באופן ספציפי, נקודת הכניסה של מעבד שעולה אונליין או חוזר ממצב השהיה נכתבת מחדש כך שטבלת הדפים של שלב 2 מותקנת ב-EL2 לפני החזרה למארח ב-EL1. במהלך האתחול, ההגנה הזו נאכפת על ידי pKVM.
הארכיטקטורה הזו מסתמכת על SoC שתומך ב-PSCI, רצוי באמצעות שימוש בגרסה עדכנית של TF-A כקושחת EL3.
התקן Firmware Framework for Arm (FF-A) מתקנן אינטראקציות בין העולם הרגיל לבין העולם המאובטח, במיוחד בנוכחות היפר-ויז'ר מאובטח. חלק גדול מהמפרט מגדיר מנגנון לשיתוף זיכרון עם העולם המאובטח, באמצעות פורמט הודעה משותף ומודל הרשאות מוגדר היטב לדפים הבסיסיים. pKVM מעביר הודעות FF-A כדי לוודא שהמארח לא מנסה לשתף זיכרון עם הצד המאובטח שאין לו הרשאות מספיקות לגביו.
הארכיטקטורה הזו מסתמכת על תוכנת העולם המאובטח שמכתיבה את מודל הגישה לזיכרון, כדי לוודא שאפליקציות מהימנות ותוכנות אחרות שפועלות בעולם המאובטח יכולות לגשת לזיכרון רק אם הוא בבעלות בלעדית של העולם המאובטח או אם הוא שותף איתו באופן מפורש באמצעות FF-A. במערכת עם S-EL2, האכיפה של מודל הגישה לזיכרון צריכה להתבצע על ידי Secure Partition Manager Core (SPMC), כמו Hafnium, שמנהל טבלאות דפים בשלב 2 עבור העולם המאובטח. במערכת ללא S-EL2, סביבת ה-TEE יכולה לאכוף מודל של גישה לזיכרון באמצעות טבלאות הדפים בשלב 1.
אם הקריאה ל-SMC אל EL2 היא לא קריאת PSCI או הודעה שמוגדרת על ידי FF-A, קריאות SMC שלא טופלו מועברות אל EL3. ההנחה היא שהקושחה המאובטחת (שחייבת להיות מהימנה) יכולה לטפל בבטחה ב-SMC שלא טופלו, כי הקושחה מבינה את אמצעי הזהירות שנדרשים כדי לשמור על הבידוד של pVM.
מעקב אחרי מכונות וירטואליות
crosvm הוא מנהל מכונות וירטואליות (VMM) שמריץ מכונות וירטואליות דרך ממשק KVM של Linux. מה שמייחד את crosvm הוא הדגש על בטיחות באמצעות שפת התכנות Rust וארגז חול סביב מכשירים וירטואליים כדי להגן על ליבת המארח. מידע נוסף על crosvm זמין במסמכי התיעוד הרשמיים כאן.
מתארי קבצים ו-ioctls
KVM חושף את מכשיר התווים /dev/kvm למרחב המשתמש באמצעות ioctl שמרכיבים את KVM API. פקודות ה-ioctl שייכות לקטגוריות הבאות:
- מערכת ioctls שולחת שאילתות ומגדירה מאפיינים גלובליים שמשפיעים על כל מערכת המשנה של KVM, ויוצרת מכונות pVM.
- פקודות ioctl של מכונות וירטואליות (VM) שולחות שאילתות ומגדירות מאפיינים שיוצרים מעבדים וירטואליים (vCPU) ומכשירים, ומשפיעים על מכונה וירטואלית פיזית (pVM) שלמה, כמו פריסת הזיכרון ומספר המעבדים הווירטואליים (vCPU) והמכשירים.
- פקודות ioctl של vCPU שולחות שאילתות ומגדירות מאפיינים ששולטים בפעולה של מעבד וירטואלי יחיד.
- ה-ioctls של המכשיר שולפים ומגדירים מאפיינים ששולטים בהפעלה של מכשיר וירטואלי יחיד.
כל תהליך crosvm מפעיל בדיוק מופע אחד של מכונה וירטואלית. התהליך הזה משתמש ב-ioctl של מערכת KVM_CREATE_VM כדי ליצור מתאר קובץ של מכונה וירטואלית שאפשר להשתמש בו כדי להנפיק ioctl של מכונה וירטואלית. ioctl KVM_CREATE_VCPU או KVM_CREATE_DEVICE ב-VM FD יוצר vCPU/מכשיר ומחזיר מתאר קובץ שמצביע על המשאב החדש. אפשר להשתמש ב-ioctls ב-vCPU או ב-device FD כדי לשלוט במכשיר שנוצר באמצעות ioctl ב-VM FD. במקרה של vCPU, זה כולל את המשימה החשובה של הפעלת קוד אורח.
באופן פנימי, crosvm רושם את מתארי הקבצים של ה-VM בקרנל באמצעות ממשק epoll עם הפעלה בקצה. לאחר מכן, ליבת המערכת מודיעה ל-crosvm בכל פעם שיש אירוע חדש בהמתנה באחד ממתארי הקבצים.
pKVM מוסיף יכולת חדשה, KVM_CAP_ARM_PROTECTED_VM, שאפשר להשתמש בה כדי לקבל מידע על סביבת ה-pVM ולהגדיר מצב מוגן למכונה וירטואלית. מערכת crosvm משתמשת בה במהלך יצירת pVM אם מועבר הדגל --protected-vm, כדי לשלוח שאילתה ולשריין את כמות הזיכרון המתאימה ל-firmware של pVM, ולאחר מכן כדי להפעיל מצב מוגן.
הקצאת זיכרון
אחת מהאחריות העיקריות של VMM היא להקצות את הזיכרון של המכונה הווירטואלית ולנהל את פריסת הזיכרון שלה. crosvm יוצרת פריסת זיכרון קבועה שמתוארת באופן כללי בטבלה שלמטה.
| FDT במצב רגיל | PHYS_MEMORY_END - 0x200000
|
| פינוי מקום | ...
|
| Ramdisk | ALIGN_UP(KERNEL_END, 0x1000000)
|
| ליבה | 0x80080000
|
| תוכנת אתחול | 0x80200000
|
| FDT במצב BIOS | 0x80000000
|
| כתובת הבסיס של הזיכרון הפיזי | 0x80000000
|
| קושחה של pVM | 0x7FE00000
|
| זיכרון המכשיר | 0x10000 - 0x40000000
|
הזיכרון הפיזי מוקצה באמצעות mmap והזיכרון מועבר למכונה הווירטואלית כדי לאכלס את אזורי הזיכרון שלה, שנקראים memslots, באמצעות KVM_SET_USER_MEMORY_REGION ioctl. לכן, כל הזיכרון של מכונת ה-pVM של האורח משויך למופע crosvm שמנהל אותו, ויכול לגרום להפסקת התהליך (סיום המכונה הווירטואלית) אם המארח מתחיל לאבד זיכרון פנוי. כשמפסיקים מכונה וירטואלית, ה-hypervisor מוחק את הזיכרון באופן אוטומטי ומחזיר אותו לליבת המארח.
ב-KVM רגיל, ל-VMM יש גישה לכל הזיכרון של האורח. ב-pKVM, הזיכרון של האורח לא ממופה ממרחב הכתובות הפיזי של המארח כשהוא מועבר לאורח. היוצא מן הכלל היחיד הוא זיכרון שהאורח שיתף באופן מפורש, למשל עבור מכשירי virtio.
אזורי MMIO במרחב הכתובות של האורח נשארים לא ממופים. הגישה לאזורים האלה על ידי האורח נלכדת ומובילה לאירוע קלט/פלט ב-FD של מכונת ה-VM. המנגנון הזה משמש להטמעה של מכשירים וירטואליים. במצב מוגן, האורח צריך לאשר שאזור במרחב הכתובות שלו משמש ל-MMIO באמצעות hypercall, כדי לצמצם את הסיכון לדליפת מידע מקרית.
תזמון
כל CPU וירטואלי מיוצג על ידי שרשור POSIX ומתוזמן על ידי מתזמן Linux של המארח. ה-thread קורא ל-KVM_RUN ioctl ב-vCPU FD, וכתוצאה מכך ה-hypervisor עובר להקשר של vCPU אורח. המתזמן של המארח מחשיב את הזמן שמוקצה להקשר של האורח כזמן שבו נעשה שימוש בשרשור ה-vCPU המתאים. KVM_RUN מוחזר כשמתרחש אירוע שצריך לטפל בו באמצעות VMM, כמו קלט/פלט, סיום הפרעה או עצירה של vCPU. ה-VMM מטפל באירוע וקורא שוב ל-KVM_RUN.
במהלך KVM_RUN, התהליך נשאר ניתן להקדמה על ידי מתזמן המארח, למעט ההפעלה של קוד ההיפר-ויזור EL2, שלא ניתן להקדמה. ל-pVM של האורח עצמו אין מנגנון לשליטה בהתנהגות הזו.
מכיוון שכל השרשורים של vCPU מתוזמנים כמו כל המשימות האחרות במרחב המשתמש, הם כפופים לכל המנגנונים הרגילים של QoS. באופן ספציפי, אפשר להקצות כל שרשור של vCPU למעבדים פיזיים, למקם אותו ב-cpusets, להגביר או להגביל אותו באמצעות clamping של ניצול המשאבים, לשנות את מדיניות התזמון או העדיפות שלו ועוד.
מכשירים וירטואליים
crosvm תומך במספר מכשירים, כולל:
- virtio-blk לתמונות דיסק מורכבות, קריאה בלבד או קריאה וכתיבה
- vhost-vsock לתקשורת עם המארח
- virtio-pci כפרוטוקול תעבורה של virtio
- שעון זמן אמת (RTC) מסוג pl030
- 16550a UART לתקשורת טורית
קושחה של pVM
הקושחה של pVM (pvmfw) היא הקוד הראשון שמופעל על ידי pVM, בדומה ל-ROM של אתחול של מכשיר פיזי. המטרה העיקרית של pvmfw היא לבצע אתחול של אתחול מאובטח ולגזור את הסוד הייחודי של pVM. השימוש ב-pvmfw לא מוגבל למערכת הפעלה ספציפית, כמו Microdroid, כל עוד מערכת ההפעלה נתמכת על ידי crosvm ונחתמה בצורה תקינה.
קובץ ה-binary של pvmfw מאוחסן במחיצת פלאש עם אותו שם, והוא מתעדכן באמצעות OTA.
אתחול המכשיר
רצף השלבים הבא נוסף לתהליך האתחול של מכשיר עם pKVM:
- טוען את pvmfw מהמחיצה שלו לזיכרון ומאמת את התמונה.
- ה-ABL מקבל את הסודות של Device Identifier Composition Engine (DICE) (מזהי מכשירים מורכבים (CDI) ושרשרת אישורי DICE) מ-Root of Trust.
- ה-ABL גוזר את ה-CDI הנדרשים עבור pvmfw, ומוסיף אותם לבינארי pvmfw.
- ה-ABL מוסיף צומת
linux,pkvm-guest-firmware-memoryשל אזור זיכרון שמור ל-DT, שמתאר את המיקום והגודל של הקובץ הבינארי pvmfw והסודות שהוא הפיק בשלב הקודם. - ABL מעביר את השליטה ל-Linux, ו-Linux מאתחל את pKVM.
- pKVM מבטל את המיפוי של אזור הזיכרון pvmfw מטבלאות הדפים בשלב 2 של המארח ומגן עליו מפני המארח (ומפני האורחים) לאורך זמן הפעולה של המכשיר.
אחרי הפעלת המכשיר, Microdroid מופעל לפי השלבים שבקטע רצף ההפעלה במסמך Microdroid.
אתחול של מכונת pVM
כשיוצרים pVM, crosvm (או VMM אחר) צריך ליצור memslot גדול מספיק כדי שההיפר-ויז'ר יאכלס אותו בתמונת pvmfw. בנוסף, יש הגבלה על VMM ברשימת הרישומים שאת הערך ההתחלתי שלהם הוא יכול להגדיר (x0-x14 עבור vCPU ראשי, ללא הגבלה עבור vCPU משני). האוגרים הנותרים שמורים והם חלק מממשק ה-ABI של hypervisor-pvmfw.
כשמריצים את ה-pVM, ההיפר-ויזור מעביר קודם את השליטה ב-vCPU הראשי
ל-pvmfw. הקושחה מצפה ש-crosvm יטען ליבה חתומה ב-AVB, שיכולה להיות טוען אתחול או כל תמונה אחרת, ו-FDT לא חתום לזיכרון בהיסטים ידועים. pvmfw מאמת את חתימת ה-AVB, ואם האימות מצליח, הוא יוצר עץ מכשירים מהימן מ-FDT שהתקבל, מוחק את הסודות שלו מהזיכרון ומסתעף לנקודת הכניסה של המטען הייעודי. אם אחד משלבי האימות נכשל, הקושחה מנפיקה קריאת מערכת (hypercall) של PSCI SYSTEM_RESET.
בין הפעלות, מידע על מופע ה-pVM מאוחסן במחיצה (מכשיר virtio-blk) ומוצפן באמצעות הסוד של pvmfw כדי להבטיח שאחרי הפעלה מחדש, הסוד יוקצה למופע הנכון.