Handle Ownership and Lifetimes
Windows handles are capabilities with lifetimes. Rust ownership and RAII make leaks and double closes easier to prevent.
Cheatsheet for Lesson 6.4
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
The Rust primer extension explains one owner’s cleanup duty and fallible completion with a small local example before those ideas are applied here.
A handle is not a pointer
Section titled “A handle is not a pointer”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.
1 ReadProcessMemory with handle 0x12C
This process’s handle table (kept by the kernel)
Handle 0x12Cquery, readrightsthe game’s process objectpoints toHandle 0x134query onlyrightsthe same process objectpoints toResult
ReadProcessMemoryallowedRow 0x12C includes read, so the call reaches the game’s process object.
2 ReadProcessMemory with handle 0x134
This process’s handle table (kept by the kernel)
Handle 0x12Cquery, readrightsthe game’s process objectpoints toHandle 0x134query onlyrightsthe same process objectpoints toResult
ReadProcessMemoryaccess deniedSame process object, but row 0x134 holds query rights only, so the read is refused.
3 Reading your own memory at address 0x12C
This process’s handle table (kept by the kernel)
Handle 0x12Cquery, readrightsthe game’s process objectpoints toHandle 0x134query onlyrightsthe same process objectpoints toYour process’s memory
Address 0x12Cnothing mapped: access violationA 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.
Handles have lifetimes
Section titled “Handles have lifetimes”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 0x12C2. you close 0x12C -> correct3. elsewhere, the program opens a log file Windows reuses the free slot and returns 0x12C again4. your cleanup code closes 0x12C a second time5. the log file is now closed, and nothing says whyAs a timeline of calls between your code and Windows, the same five steps look like this:
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 ties cleanup to scope
Section titled “RAII ties cleanup to scope”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:
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.
Why Drop ignores the close result
Section titled “Why Drop ignores the close result”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.
Prove cleanup with a count
Section titled “Prove cleanup with a count”The lab records the current process’s handle count, creates 128 unnamed event objects, wraps every returned handle, then drops the vector:
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
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.
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.
Why this version?
Section titled “Why this version?”Every error path receives the same cleanup behavior. Adding a new ? does not silently create a leak.
Run the proof
Section titled “Run the proof”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.
cd windows-labscargo run --release --target i686-pc-windows-msvc --bin handle_raiiThe 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.
Not every handle should be closed
Section titled “Not every handle should be closed”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.
Before wrapping a handle, answer:
- Does success return a new owned handle?
- Which function releases it?
- Which values mean failure?
- May the handle be shared across threads?
- 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.
Prevent resource leaks
What invariant should a safe wrapper maintain for an owned Windows handle?
Single ownership prevents both leaks and double-close bugs. Borrowed handles need a different type because they do not own cleanup.
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.
Plain Markdown, saved in this browser.
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.