באפרים של משתמש ודפים נעולים¶
אפליקציה מוסרת לדרייבר pointer ואורך. גם אם הכתובת היתה חוקית בזמן הקריאה, היא יכולה להשתנות, להשתחרר או להצביע לנתון אחר כאשר thread נוסף של האפליקציה פועל. נפריד בין בדיקת נגישות, לכידת תוכן, נעילת דפים ומיפוי לכתובת שהדרייבר יכול להשתמש בה בהקשר אחר.
האפשרות הפשוטה ביותר לנתון קטן היא להעתיק אותו לאחסון של הדרייבר ולפרש רק את העותק. הקטע הבא הוא helper מלא לשילוב בדרייבר WDK. הוא מיועד ל־buffer של בקשת UserMode כאשר אנחנו עדיין בהקשר התהליך המתאים, ומוגבל ל־64 KiB. הוא אינו מוצג כתחליף ל־buffering method של I/O manager.
#include <ntddk.h>
_IRQL_requires_max_(APC_LEVEL)
NTSTATUS CaptureUserBytes(_In_reads_bytes_(Bytes) const VOID* UserBuffer,
_In_ SIZE_T Bytes, _In_ KPROCESSOR_MODE RequestorMode,
_Outptr_result_bytebuffer_maybenull_(Bytes) PUCHAR* Output)
{
*Output = NULL;
if (RequestorMode != UserMode) return STATUS_INVALID_PARAMETER;
if (Bytes > 64u * 1024u) return STATUS_INVALID_BUFFER_SIZE;
if (Bytes == 0) return STATUS_SUCCESS;
PUCHAR copy = ExAllocatePool2(POOL_FLAG_NON_PAGED, Bytes, 'cLmA');
if (copy == NULL) return STATUS_INSUFFICIENT_RESOURCES;
NTSTATUS status = STATUS_SUCCESS;
__try {
ProbeForRead(UserBuffer, Bytes, 1);
RtlCopyMemory(copy, UserBuffer, Bytes);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
status = GetExceptionCode();
}
if (!NT_SUCCESS(status)) {
ExFreePoolWithTag(copy, 'cLmA');
return status;
}
*Output = copy;
return STATUS_SUCCESS;
}
ה־ProbeForRead בודק את התנאים המתועדים של טווח משתמש ויישור, אבל אינו מקפיא את הטווח. לכן גם ההעתקה עצמה נמצאת בתוך __try. תהליכון אחר עדיין יכול לשנות הרשאות או mapping אחרי probe. ה־SEH הוא מנגנון הטיפול של הקוד הזה בגישה שעלולה לזרוק חריגה, ואינו C++ exception שחוצה שכבות לפי מודל של תוכנת משתמש.
העותק אינו בהכרח snapshot אטומי של כל ה־buffer בזמן יחיד. אם המשתמש משנה byte-ים בזמן ההעתקה, נקבל תערובת אפשרית של מצבים. היתרון הוא שאחרי שההעתקה הסתיימה, הקלט שבבעלות הדרייבר יציב עבור validation ועיבוד. לכן נבדוק lengths, offsets, flags ויחסים בין שדות מתוך העותק, ולא נבדוק header אחד אצל המשתמש ואז נקרא אותו שוב בהמשך כדי להחליט כמה להעתיק.
בכותרת שמכילה length ו־pointer נוסף קיימת בעיית double fetch. הקריאה הראשונה יכולה לראות אורך קטן שעובר בדיקה, והקריאה השנייה יכולה לראות אורך גדול יותר. נלכוד את הכותרת פעם אחת, נבדוק את האורך שנלכד, ונשתמש בו כגבול לכל הפעולות הבאות. מצביע מקונן דורש לכידה או probing נפרד לפי החוזה; העתקת struct שמכיל מספרי כתובות אינה מאמתת את האזורים שאליהם הם מצביעים.
ה־MDL מתאר דפים פיזיים של טווח זיכרון. נעילת הדפים באמצעות המנגנון המתאים יכולה להבטיח שהם נשארים resident, אבל אינה מבטיחה שכתובת המשתמש המקורית תהיה תקפה בכל process context ואינה מונעת מן המשתמש לשנות את התוכן. ה־MmGetSystemAddressForMdlSafe יכול לספק mapping לכתובת מערכת, והוא עלול להיכשל. גם במקרה הצלחה נשמור את ה־mapping ואת הדפים לפי חיי ה־MDL, ולא נשתמש בהם אחרי unlock או completion שסיים את הבעלות.
נבדיל מי יצר את ה־MDL. בבקשת direct I/O ה־I/O manager מנהל חלק מן המחזור; אין לשחרר באופן ידני משאב שהמסגרת עדיין מצפה לנהל. כאשר הדרייבר יוצר MDL ונועל דפים בעצמו, הוא חייב לתאם unlock ו־free בכל מסלול. קריאה ל־MmProbeAndLockPages דורשת __try/__except בהתאם לחוזה שלה. ה־MDL אינו רשימת כתובות DMA שניתן לתת לחומרה בלי לעבור דרך API ה־DMA המתאים.
התיעוד על ProbeForRead ועל שימוש ב־MDL מגדיר את המנגנונים. בביקורת קוד נשאל ארבע שאלות נפרדות: האם הטווח נגיש, האם התוכן יציב, האם הכתובת מתאימה להקשר, ומי מחזיק את הדפים עד סוף השימוש.