Skip to content
Lesson 6.4Windows Processes and Callable Interfaces

Handle Ownership and Lifetimes

Windows handles are capabilities with lifetimes. Rust ownership and RAII make leaks and double closes easier to prevent.

Lesson 6.4
Read or listenPrint chapter or book
About 25 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 6.4Key ideas, reference and questions

A one-page summary of “Handle Ownership and Lifetimes” you can keep beside your notes. Plain Markdown.

Lesson 6.4 — Handle Ownership and Lifetimes: cheatsheet

  • Remember: Borrowers may use a resource temporarily; one owner remains responsible for releasing it.
  • Also useful: An owning wrapper is a cleanup receipt tied to a Rust value: when the value leaves scope, its resource has a designated release path.

Key terms

  • Resource Acquisition Is Initialization: RAII means Resource Acquisition Is Initialization.

Review

  • Q: What invariant should a safe wrapper maintain for an owned Windows handle? A: Exactly one owner closes a valid handle exactly once
  • Q: What is the numeric value of a handle that a program holds? A: An index-like token the system looks up in a per-process table
Download cheatsheet (.md)

The Rust primer extension explains one owner’s cleanup duty and fallible completion with a small local example before those ideas are applied here.

The previous lesson opened a process with a specific set of rights. Now ask who is responsible for releasing that permission-bearing reference. A tool that handles denial correctly can still leak resources or close a handle twice if ownership is vague.

Windows keeps kernel-managed objects for processes, threads, files, events, snapshots, and many other resources. A user-mode program normally refers to one of those objects through a handle.

The numeric handle value is an index-like token meaningful to Windows in the current process: Windows uses it to find a row in that process’s handle table, and the row points to the event, file, process, thread, or snapshot object. It is not the object’s memory address, and reading memory at that number does not reveal the object.

A handle entry also carries granted access rights. Two handles referring to the same process can permit different operations.

Two handles to one process, with different rightsWindows looks a handle value up in the calling process’s table. The row holds the rights and points to the kernel object.
  1. 1 ReadProcessMemory with handle 0x12C

    This process’s handle table (kept by the kernel)

    Handle 0x12C
    query, readrights
    the game’s process objectpoints to
    Handle 0x134
    query onlyrights
    the same process objectpoints to

    Result

    ReadProcessMemory
    allowed

    Row 0x12C includes read, so the call reaches the game’s process object.

  2. 2 ReadProcessMemory with handle 0x134

    This process’s handle table (kept by the kernel)

    Handle 0x12C
    query, readrights
    the game’s process objectpoints to
    Handle 0x134
    query onlyrights
    the same process objectpoints to

    Result

    ReadProcessMemory
    access denied

    Same process object, but row 0x134 holds query rights only, so the read is refused.

  3. 3 Reading your own memory at address 0x12C

    This process’s handle table (kept by the kernel)

    Handle 0x12C
    query, readrights
    the game’s process objectpoints to
    Handle 0x134
    query onlyrights
    the same process objectpoints to

    Your process’s memory

    Address 0x12C
    nothing mapped: access violation

    A handle value is not an address. The low 64 KiB of a process are never mapped, so this read faults instead of showing the object.

Windows looks a handle value up in the calling process’s table. The row holds the rights and points to the kernel object.

Many APIs return a new handle that the caller owns. When the program is finished with it, the owner calls CloseHandle. Forgetting causes a handle leak. Repeated leaks can exhaust resources or keep files and processes alive longer than expected.

Closing the same handle twice is also wrong. Windows may have already reused that number for a different object, turning a cleanup mistake into a difficult intermittent bug.

That sequence is worth spelling out, because it explains why this class of bug surfaces so far away from its cause:

1. you open a process and receive handle 0x12C
2. you close 0x12C -> correct
3. elsewhere, the program opens a log file
Windows reuses the free slot and returns 0x12C again
4. your cleanup code closes 0x12C a second time
5. the log file is now closed, and nothing says why

As a timeline of calls between your code and Windows, the same five steps look like this:

WindowsYour codeWindowsYour codecorrect — slot 0x12C is now freethe log file closes instead,and nothing reports an errorOpenProcess(...)handle 0x12C (the process)CloseHandle(0x12C)CreateFile(log file)handle 0x12C (the log file)CloseHandle(0x12C) again

Step 4 does not report an error. It closes something perfectly real — just not the object you meant. The failure appears later, in unrelated code, at a moment that depends on the exact order in which the program happened to allocate handles. That timing dependence is precisely why double-close bugs are so hard to reproduce and so easy to blame on the wrong component.

A handle is closed exactly once by its owner. Forgetting leaks it; closing twice can close whatever Windows reused the number for, such as a log file. The interactive version needs JavaScript; the lesson text around it explains the same idea.

The ownership rule should be simple:

One owner closes one owned handle exactly once. Borrowers may use it temporarily but never close it.

The word owned matters because not every handle-shaped value is released with CloseHandle. Some APIs return pseudo-handles or borrowed handles that must not be passed to CloseHandle; other APIs use specialized cleanup functions. The wrapper constructor should document which API created the value and which function releases it.

Model those categories with different types instead of one universal HANDLE wrapper. A type that always closes is safe only for resources whose contract says the caller owns a closable handle.

RAII means Resource Acquisition Is Initialization. A value owns the resource, and its Drop implementation releases the resource when the value leaves scope.

The ownership path is deliberately one-way:

Windows API succeeds
Raw owned handle
OwnedHandle
Borrow for API calls
Scope ends
Drop calls CloseHandle

Borrowing permits temporary use; only the owner reaches the cleanup path.

The course wrapper is intentionally small:

#[derive(Debug)]
pub struct OwnedHandle(HANDLE);
impl OwnedHandle {
pub fn from_raw(handle: HANDLE) -> windows::core::Result<Self> {
if handle.is_invalid() {
return Err(windows::core::Error::from_thread());
}
Ok(Self(handle))
}
pub fn raw(&self) -> HANDLE {
self.0
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
// SAFETY: this value owns the handle and never closes it elsewhere.
let _ = unsafe { CloseHandle(self.0) };
}
}

OwnedHandle does not implement Copy or Clone. Moving it transfers ownership, so the old variable cannot be used afterward. Borrowing &OwnedHandle lets another function call Windows without taking responsibility for cleanup.

RAII connects resource lifetime to lexical scope, including early returns and ? errors. It does not decide whether the original ownership contract was correct. Wrapping a borrowed handle as owned merely turns the eventual double-close into an automatic bug.

Drop::drop cannot return Result. Cleanup often happens while another error is already unwinding the stack. The course therefore performs a best-effort CloseHandle and keeps the original error.

A close failure normally means the ownership invariant was already broken, such as an invalid or previously closed handle. The fix is to correct ownership, not to retry closing an unknown numeric value.

The lab records the current process’s handle count, creates 128 unnamed event objects, wraps every returned handle, then drops the vector:

handle_count(): before
create 128 events,
each wrapped in OwnedHandle
handle_count(): while_owned
drop(events)
handle_count(): after_drop

The lab counts the process’s handles before, while 128 event handles are owned, and after they are dropped. Wrapped in OwnedHandle the count falls back; raw handles that are never closed leave the count raised. The interactive version needs JavaScript; the lesson text around it explains the same idea.

Complete lab source: handle_raii.rs

handle_raii.rs

windows-labs/src/bin/ · Rust, 48 lines

Show the codeHide the code
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
    use gha_windows_labs::OwnedHandle;
    use windows::Win32::System::Threading::{
        CreateEventW, GetCurrentProcess, GetProcessHandleCount,
    };

    fn handle_count() -> windows::core::Result<u32> {
        let mut count = 0_u32;
        // SAFETY: GetCurrentProcess returns this process's pseudo-handle and
        // `count` remains a valid output pointer for the duration of the call.
        unsafe { GetProcessHandleCount(GetCurrentProcess(), &mut count)? };
        Ok(count)
    }

    let before = handle_count()?;
    let events: Vec<OwnedHandle> = (0..128)
        .map(|_| {
            // SAFETY: no security structure or name pointer is supplied. Windows
            // creates a new unnamed, auto-reset, initially nonsignaled event.
            let raw = unsafe { CreateEventW(None, false, false, None) }?;
            OwnedHandle::from_raw(raw)
        })
        .collect::<windows::core::Result<_>>()?;
    let while_owned = handle_count()?;

    drop(events);
    let after_drop = handle_count()?;

    println!("Before:      {before} handles");
    println!("While owned: {while_owned} handles");
    println!("After drop:  {after_drop} handles");
    anyhow::ensure!(
        while_owned >= before.saturating_add(128),
        "the event handles were not visible in the process count"
    );
    anyhow::ensure!(
        after_drop < while_owned,
        "dropping OwnedHandle did not reduce the handle count"
    );
    println!("Rust dropped the vector, and each OwnedHandle called CloseHandle once.");
    Ok(())
}

#[cfg(not(windows))]
fn main() {
    eprintln!("This RAII handle lab must run on Windows.");
}
#[cfg(windows)]
fn main() -> anyhow::Result<()> {
use gha_windows_labs::OwnedHandle;
use windows::Win32::System::Threading::{
CreateEventW,
GetCurrentProcess,
GetProcessHandleCount,
};
fn handle_count() -> windows::core::Result<u32> {
let mut count = 0_u32;
// SAFETY: GetCurrentProcess returns this process's pseudo-handle and
// count remains a valid output pointer during the call.
unsafe {
GetProcessHandleCount(GetCurrentProcess(), &mut count)?;
}
Ok(count)
}
let before = handle_count()?;
let events: Vec<OwnedHandle> = (0..128)
.map(|_| {
// SAFETY: no security structure or name pointer is supplied.
let raw = unsafe { CreateEventW(None, false, false, None) }?;
OwnedHandle::from_raw(raw)
})
.collect::<windows::core::Result<_>>()?;
let while_owned = handle_count()?;
drop(events);
let after_drop = handle_count()?;
println!("Before: {before} handles");
println!("While owned: {while_owned} handles");
println!("After drop: {after_drop} handles");
anyhow::ensure!(
while_owned >= before.saturating_add(128),
"the event handles were not visible in the process count",
);
anyhow::ensure!(
after_drop < while_owned,
"dropping OwnedHandle did not reduce the handle count",
);
println!("Each OwnedHandle called CloseHandle once.");
Ok(())
}
#[cfg(not(windows))]
fn main() {
eprintln!("This RAII handle lab must run on Windows.");
}

The iterator returns Result<OwnedHandle> for every event. collect::<Result<Vec<_>, _>>() stops on the first failure. Any handles already collected live in the partially built vector, so they are dropped while the error returns.

That is the important improvement over manual cleanup:

The original helper creates events and stores raw HANDLE values. If a later creation fails, ? returns before the earlier handles are closed; dropping a vector of raw handles does not call CloseHandle. Even success makes cleanup the caller’s job.

Before: accumulate raw event handles
fn create_events(count: usize) -> windows::core::Result<Vec<HANDLE>> {
let mut raw_handles = Vec::new();
for _ in 0..count {
// SAFETY: all pointer arguments are absent; Windows returns the handle.
let raw = unsafe { CreateEventW(None, false, false, None) }?;
raw_handles.push(raw); // A later error leaks the earlier events.
}
Ok(raw_handles)
}

RAII gives each successful creation an owner immediately. Collecting the results drops those owners if a later item fails; success returns a vector whose owners close the events when dropped.

Improved code

fn create_events(count: usize) -> windows::core::Result<Vec<OwnedHandle>> {
(0..count)
.map(|_| {
// SAFETY: all pointer arguments are absent; Windows returns the handle.
let raw = unsafe { CreateEventW(None, false, false, None) }?;
OwnedHandle::from_raw(raw)
})
.collect() // Earlier owners are dropped if a later creation fails.
}

Show the diff

fn create_events(count: usize) -> windows::core::Result<Vec<HANDLE>> {
let mut raw_handles = Vec::new();
for _ in 0..count {
// SAFETY: all pointer arguments are absent; Windows returns the handle.
let raw = unsafe { CreateEventW(None, false, false, None) }?;
raw_handles.push(raw); // A later error leaks the earlier events.
}
Ok(raw_handles)
fn create_events(count: usize) -> windows::core::Result<Vec<OwnedHandle>> {
(0..count)
.map(|_| {
// SAFETY: all pointer arguments are absent; Windows returns the handle.
let raw = unsafe { CreateEventW(None, false, false, None) }?;
OwnedHandle::from_raw(raw)
})
.collect() // Earlier owners are dropped if a later creation fails.
}

Takeaway: put each acquired resource into its cleanup owner before acquiring the next one.

Every error path receives the same cleanup behavior. Adding a new ? does not silently create a leak.

Windows VM

Run the self-contained handle lab in the Windows VM. It prints a count before creating events, a higher count while they are owned, and another count after their owners leave scope. The change across those three moments is the proof.

Terminal window
cd windows-labs
cargo run --release --target i686-pc-windows-msvc --bin handle_raii

The middle count should rise by at least 128. The final count should fall again. Small background differences are possible because process instrumentation may briefly create its own handles, so the program checks the meaningful direction instead of demanding identical before/after numbers.

Some APIs return a pseudo-handle, such as GetCurrentProcess. It is a special constant interpreted as the current process and should not be wrapped as a newly owned handle.

Other APIs return borrowed handles whose documentation says another component owns them. Ownership comes from the API contract, not from the fact that a program can store the number.

yes, e.g. OpenProcess,
CreateEventW
no — pseudo-handle
or borrowed handle
a HANDLE value
does the API's contract say
you now own a new handle?
wrap as OwnedHandle:
Drop calls CloseHandle
never pass it to CloseHandle

Before wrapping a handle, answer:

  1. Does success return a new owned handle?
  2. Which function releases it?
  3. Which values mean failure?
  4. May the handle be shared across threads?
  5. Does an event or callback transfer ownership?

The complete wrapper is handle.rs, and the proof program is handle_raii.rs.

References: CloseHandle, GetProcessHandleCount, and kernel objects.

Knowledge check

Prevent resource leaks

What invariant should a safe wrapper maintain for an owned Windows handle?

Finished Lesson 6.4?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 6: 11 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.