What Is Really in That MDL?
A memory descriptor list (MDL) is a system-defined structure that describes a buffer by a set of physical addresses. A driver that performs direct I/O receives a pointer to an MDL from the I/O manager, and reads and writes data through the MDL. Some drivers also use MDLs when they perform direct I/O to satisfy a device I/O control request.
Driver writers should make no assumptions about the order or contents of pages that are described by an MDL. Drivers must not rely on the value of the data at any location that is referenced by an MDL and should not directly dereference a memory location to get the data. If the MDL describes a buffer for a direct I/O operation, the application that issued the I/O request might also have mapped a view of the same pages into its address space. If so, the application and the driver could try to modify the data at the same time, which causes errors.
Furthermore, in some cases the locations in the MDL do not reference the same physical pages that the memory manager retains. When the Microsoft Windows memory manager constructs an MDL for a device read, it locks physical pages to use for the transfer target. However, it is solely up to the memory manager to determine which pages to keep and which (if any) to discard. Why does the memory manager read data into these pages and then discard them? Because doing I/O in larger clusters provides better performance.
For example, in the following figure, the file offsets and VAs that correspond to pages A, Y, Z, and B are logically contiguous although the physical pages themselves are not necessarily contiguous. Pages A and B are nonresident, so the memory manager must read them. Pages Y and Z are already resident in memory, so it is not necessary to read them. (In fact, they might already have been modified since they were last read in from their backing store, in which case it would be a serious error to overwrite their contents.) However, reading pages A and B in a single operation is more efficient than performing one read for page A and a second read for page B. Therefore, the memory manager issues a single read request that comprises all four pages (A, Y, Z, and B) from the backing store. Such a read request includes as many pages as make sense to read, based on the amount of available memory, the current system usage, and so on.
When the memory manager builds the memory descriptor list (MDL) that describes the request, it supplies valid pointers to pages A and B. However, the entries for pages Y and Z point to a single system-wide dummy page X. The memory manager can fill the dummy page X with the potentially stale data from the backing store because it does not make X visible. However, if a component accesses the Y and Z offsets in the MDL, it sees the dummy page X instead of Y and Z.
The memory manager can represent any number of discarded pages as a single page, and that page can be embedded multiple times in the same MDL or even in multiple concurrent MDLs that are being used for different drivers. Consequently, the contents of the locations that represent the discarded pages can change at any time.
Drivers that perform decryption or calculate checksums that are based on the value of data on the pages that the MDL maps must not dereference pointers from the system-supplied MDL to access the data. Instead, to ensure correct operation, such a driver should create a temporary MDL that is based on the system-supplied MDL that the driver received from the I/O manager. To create the temporary MDL:
- Call MmGetMdlVirtualAddress and MmGetMdlByteCount to get the base virtual address and length of the system-supplied MDL.
- Call ExAllocatePoolWithTag with PoolType=NonPagedPool to allocate a buffer from nonpaged pool. Specify a buffer size that is the length of the system-supplied MDL, rounded up to a page boundary.
- Call IoAllocateMdl to allocate an MDL with the base virtual address and length of the pool buffer that was created in step 2.
- Call MmBuildMdlForNonPagedPool to update the temporary MDL so that it describes the underlying physical pages of the pool buffer from step 2.
The driver should pass this temporary MDL in calls that read data from its hardware and then use the values of the data that was described by the temporary MDL in any required manipulations. By calling MmBuildMdlForNonPagedPool to update the temporary MDL, the driver ensures that the temporary MDL cannot possibly have any temporary pages, thus protecting it from any changes to the contents of the pages. In this way, even if the system MDL contains (potentially duplicated) pages that will be discarded, the driver avoids examining unstable contents. When the driver completes its manipulations, it should copy the changed data from the temporary MDL back to the system-supplied MDL by using RtlCopyMemory within a try/except or try/finally block.
Drivers that use MDLs as part of a typical I/O operation without accessing the data on the underlying pages do not need to create a temporary MDL. Internally, the memory manager keeps track of all the pages that are resident and how each is mapped. When a driver passes an MDL to a system service routine to perform I/O, the memory manager ensures that the correct data is used.
What should you do?
- Do not assume that the contents of any memory location that is pointed to by an MDL are valid at any given time.
- Always double-buffer data in system-supplied MDLs if your driver depends on the value of the data.