התקן ובקשת קלט ופלט¶
אפליקציה פותחת device interface ושולחת DeviceIoControl. בדרך אל הדרייבר נוצרת בקשה שהבעלות עליה עוברת בין רכיבים ב־driver stack. נרצה לדעת מי רשאי לקרוא את ה־buffer, מי מחויב להשלים את ה־IRP ומתי אסור לגעת בו יותר. הטעות המסוכנת אינה רק חיבור שגוי של bytes; היא יכולה להיות completion כפול של אובייקט שהמערכת כבר החזירה לבעליו.
ה־DRIVER_OBJECT מחזיק בין היתר טבלת dispatch של הדרייבר. ה־DEVICE_OBJECT מייצג מופע התקן או שכבה מסוימת ב־stack. ה־device interface הוא שם גילוי וחיבור לאפליקציות, ולא כתובת של אותו מבנה בזיכרון. ה־IRP מכיל מצב בקשה ו־stack locations שמאפשרים לשכבות שונות להעביר פרמטרים ולהוסיף completion routine לפי החוזה.
נבחר IOCTL קטן שמחשב סכום של עד 256 ערכי ULONG. ה־METHOD_BUFFERED מאפשר ל־I/O manager לספק SystemBuffer, ואנחנו נבדוק בנפרד input length ו־output length. ה־access bits דורשים handle שנפתח עם הרשאות קריאה וכתיבה. בנוסף לכך ההתקן עצמו חייב להיות מוגן ב־ACL מתאים מתוך INF, KMDF או IoCreateDeviceSecure; access bits אינם מחליפים את מדיניות מי רשאי לפתוח אותו.
הקוד הבא הוא dispatch routine מלא לשילוב בדרייבר WDM שיש לו device object מוגן ושנרשם עבור IRP_MJ_DEVICE_CONTROL. הוא אינו יוצר התקן בלתי מוגן לצורך קיצור הדוגמה. בשיעור KMDF נמפה את אותו חוזה ל־KMDF וליצירת ההתקן המלאה.
#include <ntddk.h>
#define IOCTL_LAB_SUM CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA)
typedef struct _LAB_HEADER {
ULONG Size;
ULONG Version;
ULONG Count;
} LAB_HEADER;
typedef struct _LAB_RESULT {
ULONGLONG Sum;
ULONG Count;
ULONG Reserved;
} LAB_RESULT;
DRIVER_DISPATCH LabDeviceControl;
_Use_decl_annotations_
NTSTATUS LabDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
UNREFERENCED_PARAMETER(DeviceObject);
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG_PTR information = 0;
if (stack->Parameters.DeviceIoControl.IoControlCode == IOCTL_LAB_SUM) {
ULONG input = stack->Parameters.DeviceIoControl.InputBufferLength;
ULONG output = stack->Parameters.DeviceIoControl.OutputBufferLength;
PUCHAR buffer = Irp->AssociatedIrp.SystemBuffer;
status = STATUS_INVALID_BUFFER_SIZE;
if (buffer != NULL && input >= sizeof(LAB_HEADER) && output >= sizeof(LAB_RESULT)) {
LAB_HEADER header;
RtlCopyMemory(&header, buffer, sizeof(header));
if (header.Version == 1 && header.Count <= 256 && header.Size == input &&
input == sizeof(LAB_HEADER) + header.Count * sizeof(ULONG)) {
LAB_RESULT result = { 0 };
for (ULONG index = 0; index < header.Count; ++index) {
ULONG value;
RtlCopyMemory(&value, buffer + sizeof(LAB_HEADER) + index * sizeof(ULONG),
sizeof(value));
result.Sum += value;
}
result.Count = header.Count;
RtlCopyMemory(buffer, &result, sizeof(result));
information = sizeof(result);
status = STATUS_SUCCESS;
}
}
}
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = information;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
ה־SystemBuffer משותף לקלט ולפלט בשיטה הזאת, ולכן אנחנו מעתיקים את הכותרת ומסיימים לקרוא את כל הערכים לפני כתיבת התוצאה. ה־Count נבדק לפני הכפל, והתקרה הקטנה מוכיחה שהחישוב של האורך אינו גולש. הסכום של 256 ערכי 32 ביט נכנס ל־ULONGLONG. ה־Reserved מאופס, ו־Information מציין רק את מספר ה־byte-ים שאותחלו והוחזרו. מסלול כשל מחזיר אפס במקום לחשוף bytes לא מאותחלים.
ההשלמה מתרחשת פעם אחת בסוף, ואחריה אין גישה נוספת ל־Irp. היא אינה מתבצעת בזמן החזקת spin lock. כאשר דרייבר מעביר בקשה לשכבה נמוכה יותר, הוא צריך להשתמש בכללי stack location ו־IoCallDriver, ולא להשלים מיד את אותה בקשה כאילו הבעלות נשארה רק אצלו. שגרת completion יכולה לשנות את זרימת ההשלמה לפי status מתאים, ולכן צריך לקרוא את החוזה של כל מסלול ולא לשכפל תבנית באופן עיוור.
בקשה שנשארת pending דורשת מצב חיים נוסף. הדרייבר מסמן ומחזיר STATUS_PENDING לפי החוזה, מחזיק את הבקשה בתור שמטפל בביטול, ומשלים אותה מאוחר יותר בדיוק פעם אחת. מרוץ בין cancel לבין worker מחייב הכרעה מי לקח ownership של completion. שדה boolean לא מסונכרן אינו מספיק; נשתמש במנגנון cancel-safe queue או במסגרת KMDF המתאימה ונבדוק teardown בזמן בקשה פעילה.
ה־IOCTL נבדק במלוא 32 הביטים, ולא רק לפי FunctionCode. שינוי method או access bits משנה את החוזה. אין לבנות IOCTL שמקבל כתובת kernel שרירותית וקורא או כותב בה בשם המשתמש. התיעוד על שיטות גישה ל־buffers ועל אבטחת IOCTL משלים את הבדיקה של lengths, הרשאות וחיי בקשה.