> For the complete documentation index, see [llms.txt](https://7-stars-servers.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://7-stars-servers.gitbook.io/docs/windows-internals/memory-descriptor-lists.md).

# Memory Descriptor Lists

### 1. What's an MDL

An **MDL (Memory Descriptor List)** is a kernel structure used by the Windows operating system to describe how a region of **virtual memory** maps to **physical memory pages**. It acts as a bridge that lets kernel-mode components such as drivers safely and efficiently access memory regions that may belong to user-mode processes.

Because kernel-mode drivers cannot directly access user-mode memory (for both security and stability reasons), the OS uses MDLs to provide a safe reference to the actual physical pages that back a user-space buffer. When a driver receives a pointer to user memory, the system creates an MDL that records which physical pages correspond to that virtual range, allowing the driver to perform I/O or DMA operations without relying on user-space virtual addresses.

To understand why MDLs are necessary, it’s essential to grasp the **difference between virtual and physical memory**:

* **Virtual memory** gives each process a continuous address space. From the program’s perspective, the buffer appears as one uninterrupted block of memory.
* **Physical memory**, however, is fragmented. The physical RAM pages that back that virtual range may be scattered throughout the system.

MDLs resolve this mismatch by providing a page-by-page map that links each portion of the virtual buffer to its physical page frame. When performing I/O operations, especially those involving DMA (Direct Memory Access), the system “locks” the relevant pages in memory to prevent them from being paged out and then uses the MDL to communicate their exact physical layout to the hardware or kernel subsystem.

In short:

* **Purpose**: Safely expose user-mode or pageable kernel buffers to kernel-mode or hardware.
* **Function**: Describe which physical pages back a given virtual buffer.
* **Use case**: Common in I/O operations, DMA transfers, and data movement between user space and kernel space.

<figure><img src="https://1403196296-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7iDlIelmS5trhjoQBiFK%2Fuploads%2FVjxJwSf04JPK1r2OmAPF%2Fimage.png?alt=media&amp;token=a299bf94-c334-42c7-bec3-047f4c9e9ce4" alt=""><figcaption></figcaption></figure>

**TL;DR:**\
The MDL is a kernel-side “map” that translates a user-mode buffer’s virtual memory into the physical pages it occupies, allowing kernel drivers or DMA hardware to access that memory safely and efficiently.

### 2. Why MDLs are important

MDLs exist to let the kernel (and devices) work with user buffers efficiently and safely without doing wasteful copies.

#### Problem: user memory is not directly usable by the kernel

User-mode code allocates a buffer at some virtual address. That buffer lives in user space:

* The kernel cannot just trust or dereference that pointer.
* The memory could get paged out or moved.
* Hardware doing DMA cannot use a user-mode virtual address at all. Hardware only understands physical memory.

The naive workaround is: copy the data into a separate kernel buffer first.

That works but it is slow for large I/O.

***

#### Scenario 1. No MDL, simple buffered I/O

Flow:

1. User allocates `userBuffer` in user-mode virtual memory.
2. Driver gets called and needs that data.
3. Kernel allocates a new `kernelBuffer` in nonpaged kernel memory.
4. Kernel copies with something like:

   ```
   memcpy(kernelBuffer, userBuffer, size);
   ```
5. The driver now works on `kernelBuffer`, not `userBuffer`.

Cost:

* An extra allocation in kernel space.
* A full memory copy on the CPU.
* For write-back, maybe a second copy in the other direction.

This is fine for small control messages. It is wasteful for large data transfers like disk or network payloads.

***

#### Scenario 2. MDL + Direct I/O (no extra copy)

Goal:\
Use the same physical pages that back the user buffer, without copying them into a new kernel buffer.

Flow:

1. The user buffer is already backed by physical RAM pages.
2. The driver (or I/O manager) builds an MDL for that buffer. The MDL:
   * Locks those pages in RAM so they cannot be paged out.
   * Records the physical page frame numbers for each page.
3. The driver uses the MDL instead of doing a copy.

From here there are two common cases:

* **Driver wants to read/write the buffer in kernel code**

  The driver can map the MDL into system (kernel) address space using something like `MmGetSystemAddressForMdlSafe`.\
  Result: the driver now has a valid kernel-mode VA that points to the same physical memory the user allocated.\
  No copy. Just direct access.
* **Driver wants the device to DMA directly**

  The MDL tells the OS which physical pages belong to the buffer. The OS can translate that into a scatter/gather list for the device.\
  The device can then DMA straight into or out of those physical pages.\
  Again no copy.

### 3. How MLDs work

When a user-mode program allocates a buffer, that buffer’s **virtual address (VA)** points to pages of **physical memory** that the operating system manages. Kernel-mode code cannot safely or directly access that memory. **MDLs (Memory Descriptor Lists)** provide the formal mechanism to describe and lock those pages so the kernel or device drivers can use them.

***

#### 1. Creating the MDL

A driver first allocates an MDL structure that will describe the user buffer.

```
PMDL mdl = IoAllocateMdl(userBuffer, bufferSize, FALSE, FALSE, NULL);

```

**What this does:**

* Allocates an MDL data structure in nonpaged pool.
* Records the starting virtual address and length of the buffer.
* Does *not yet* make the memory usable—it just reserves the MDL object.

The MDL will later be filled with the actual physical page frame mappings.

***

#### 2. Locking the Pages

Before the driver touches the buffer, the OS must **lock** those pages in RAM so they can’t be paged out or moved:

```
MmProbeAndLockPages(mdl, UserMode, IoReadAccess);

```

**Meaning:**

* The function verifies that the virtual range is valid and accessible.
* It pins each page in physical memory.
* It populates the MDL with the physical page frame numbers that back that virtual range.

After this call, the MDL fully represents the physical memory layout of the user buffer.\
The OS guarantees those pages stay resident until the driver unlocks them.

***

#### Practical Result

Now the driver (or DMA controller) can:

* Map the MDL into system space with `MmGetSystemAddressForMdlSafe()` and access the buffer directly in kernel mode.
* Or use the MDL for a DMA operation, where the hardware reads or writes those physical pages directly.

When finished, the driver must:

```
MmUnlockPages(mdl);
IoFreeMdl(mdl);
```

Each issue arises because MDLs let kernel and user share pages. That shared access creates race, trust, and lifetime problems. Below I explain the exact mechanics, how an attacker exploits each, and concrete mitigations and safe coding patterns.

### 4. MLDs security

#### 1) Double-mapping / TOCTOU (Time-of-Check → Time-of-Use)

What happens exactly

* Driver receives a user pointer and inspects (validates) the contents in user space.
* Later the driver locks pages or maps them via an MDL and then *uses* the data.
* Between the validation and the actual use the user process can change the buffer contents because the pages remain writable by user code. Locking pins pages but does not prevent their contents from changing.

How an attacker exploits it

* The attacker runs code in the user process that flips fields in the buffer between the check and use windows.
* Example exploit: driver checks `isAdmin = userStruct->isAdmin` and skips privilege checks; attacker flips `isAdmin` after check but before action. Kernel acts on stale validated data, enabling privilege escalation or logic bypass.
* The attacker uses threads, signals, or `clone`/`fork` timing to increase the chance of the race.

Concrete mitigations

* Do atomic validation+use inside kernel or re-validate after mapping. In practice:
  * **Probe/lock → map → validate → use**. Probe and lock before any validation that matters. Then map and re-check critical fields using the kernel-mapped pointer.
  * Or **copy to a kernel buffer** and operate on the copy for any security decision.
* Keep the check and use as close together as possible and at the same IRQL where no user code can run in between.
* Use `KeRaiseIrql` / proper locking only where appropriate; avoid long gaps between lock and use.
* Example safe pattern:

  ```
  PMDL mdl = IoAllocateMdl(userPtr, len, FALSE, FALSE, NULL);
  __try { MmProbeAndLockPages(mdl, UserMode, IoReadAccess); }
  __except(EXCEPTION_EXECUTE_HANDLER) { IoFreeMdl(mdl); return STATUS_INVALID_PARAMETER; }
  kernelPtr = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority);
  // Re-validate fields using kernelPtr now that it is pinned and mapped
  if (!validate(kernelPtr)) { MmUnlockPages(mdl); IoFreeMdl(mdl); return STATUS_ACCESS_DENIED; }
  // Use kernelPtr safely
  MmUnlockPages(mdl); IoFreeMdl(mdl);
  ```

#### 2) Incorrect AccessMode in `MmProbeAndLockPages()`

What happens exactly

* `MmProbeAndLockPages(mdl, AccessMode, Operation)` uses `AccessMode` to decide whether to probe user addresses.
* If caller passes `KernelMode` for a user-supplied pointer, the kernel will *not* probe the pointer and will assume it is safe.
* That lets invalid or malicious addresses pass through. The driver may then lock or map unexpected physical pages or cause faults later.

How an attacker exploits it

* Provide a user pointer that points into kernel memory or to a special address. Because no probe occurs, the driver might lock or map kernel addresses leading to:
  * Kernel memory corruption when driver writes to that mapped pointer.
  * System instability and BSOD if invalid addresses are touched.
  * In some designs, route device DMA into kernel pages or overwrite sensitive kernel structures.
* If the driver later dereferences the mapped pointer without proper checks it may read secrets or overwrite kernel data.

Concrete mitigations

* Always pass `UserMode` when probing user-supplied addresses.
* Wrap `MmProbeAndLockPages` in `try/except` to handle exceptions.
* Validate the buffer origin and length before probing and locking.
* Minimal safe code:

  ```
  __try {
    MmProbeAndLockPages(mdl, UserMode, IoReadAccess);
  } __except(EXCEPTION_EXECUTE_HANDLER) {
    IoFreeMdl(mdl);
    return STATUS_ACCESS_VIOLATION;
  }
  ```
* Never mix assumptions (don’t treat user pointers as kernel pointers).

#### 3) Use-After-Free (UAF) with MDLs

What happens exactly

* An MDL describes specific PFNs (page frames). The MDL must be unlocked/freed when the consumer is done.
* UAF arises in two patterns:
  1. **Driver frees MDL or unpins pages and later still uses the mapped pointer.**
  2. **The underlying process frees or reuses the virtual region while the driver still expects the MDL to reference stable content.** (If pages were not properly pinned or if the driver relied on assumptions about lifetime.)

How an attacker exploits it

* If driver unpins/frees but keeps a pointer, the page frames can be reallocated by the OS or another process. Attacker allocates an object at the new physical frames and controls contents. Driver dereferences stale pointer and operates on attacker-controlled data → code execution, privilege escalation, data corruption.
* If driver frees MDL but the device still DMA’s to that MDL, attacker may direct DMA to controlled pages.

Concrete mitigations

* **Pin pages** with `MmProbeAndLockPages` and do not `MmUnlockPages` until driver is done.
* Use reference counting and track ownership carefully. Do not keep the kernel mapping beyond the scope documented.
* If async operations are involved (IRPs, DMA), ensure the MDL is kept alive until DMA completion:
  * Increment IRP reference counts or hold a driver-specific reference until completion callback.
  * In KMDF use the framework primitives that hold buffers for you.
* Clear and zero sensitive memory before unlocking if re-use could leak secrets.
* Example safe DMA pattern:
  * Allocate MDL and lock pages.
  * Build scatter/gather list referencing the MDL.
  * Start DMA.
  * In DMA completion routine: stop device, ensure DMA finished, then `MmUnlockPages` and `IoFreeMdl`.
* Audit code paths for every return and error path to ensure `MmUnlockPages`/`IoFreeMdl` happen exactly once and only after the device/driver is fully done.

#### Practical checklist for reversing or auditing a driver that uses MDLs

* Verify `AccessMode` used in `MmProbeAndLockPages` is `UserMode` for user buffers.
* Find sequences: **validate → lock/map → use**. Confirm validation happens *after* lock/map.
* Check for try/except around `MmProbeAndLockPages` calls.
* Track MDL lifetime across async operations. Confirm MDL is referenced until DMA/IRP completion.
* Look for direct `MmGetSystemAddressForMdlSafe` usage and ensure pointer stored in long-lived global state is not used after unlock.
* Confirm that any security decisions are made on kernel-copied or re-validated data.
* Look for attempts to switch page protections (e.g., make pages read-only) — those are safer for checks but still require re-validation.
