All Articles

Using User-Mode APCs with the Windows API in Rust

This page has been machine-translated from the original page.

Continuing from the previous articles, I wanted to keep trying out various programs that call the Windows API from Rust.

This time, I wrote a program in Rust that creates a thread and triggers an APC function registered to that thread.

Table of contents

About Windows Asynchronous Procedure Calls (APC)

Windows Asynchronous Procedure Calls (APC) are one of the mechanisms that can execute work asynchronously on a specific thread.

APCs are a mechanism that can run user-mode programs and system code in the context of a specific user thread (that is, within the execution address space of a particular process).

Reference: Asynchronous Procedure Calls - Win32 apps | Microsoft Learn

APCs are represented by kernel objects called APC objects, which are registered in one of the two APC queues that exist for each thread and wait there to be executed.

Of the two APC queues that each thread maintains, one is a queue for kernel APCs and the other is a queue for user APCs.

The APC objects registered in these queues wait to be executed via a software interrupt.

Types of APCs

Broadly speaking, APCs come in two modes: user-mode APCs and kernel-mode APCs.

A user-mode APC is an APC generated by an application, while a kernel-mode APC is an APC generated by the system.

In addition, each mode has both Normal and Special APCs.

The differences in behavior between each mode are described in considerable detail on pp. 67-69 of Inside Windows, 7th Edition, Part 2, but that is not the point I want to focus on here, so I will omit it.

When APCs run

APCs are usually executed when a thread enters an Alertable state.

In particular, for user-mode APCs, as described in the documentation below, the APC function runs when a call such as SleepEx moves the thread into an Alertable state.

Reference: Asynchronous Procedure Calls - Win32 apps | Microsoft Learn

This time, I will aim to implement a program in Rust that uses this user-mode APC mechanism.

How to register a user-mode APC

Registering a user-mode APC is very simple: just call the QueueUserAPC API with a PAPCFUNC, which is a pointer to the APC function you want to execute, and a handle to the thread where you want to register the APC.

Reference: QueueUserAPC function (processthreadsapi.h) - Win32 apps | Microsoft Learn

This makes the APC function execute when the target thread changes to an Alertable state.

Code for registering the user-mode APC I created

To verify the behavior of a user-mode APC, I wrote the following code in Rust.

// cargo run --bin UserModeApc

use std::{thread, time};
use windows::Win32::System::Threading::{GetCurrentThread, GetCurrentThreadId, QueueUserAPC, SleepEx};

extern "system" fn apc_func(_: usize)  {
    println!("User mode APC triggered !!");
    return;
}

fn non_alertable_func() {

    let sleep_time: time::Duration = time::Duration::from_millis(5000);
    let now = time::Instant::now();
    
    println!("Non alertable sleep started.");
    thread::sleep(sleep_time);
    assert!(now.elapsed() >= sleep_time);
    println!("Non alertable sleep started.");

    return;
}

fn alertable_func() {

    let now = time::Instant::now();
    
    // SleepEx による Alertable な Sleep
    println!("Alertable sleep started.");
    unsafe {
        SleepEx(9999999, true);
    }
    // APC がコールさ SleepEx が中断されるので実際には長時間の Sleep は発生しない
    println!("Alertable sleep fineshed in {} nanos.", now.elapsed().as_nanos());

    return;
}


fn main() {

    let non_alertable_thread_handle = thread::spawn(|| {

        unsafe {
            let h_spawned_thread = GetCurrentThread();
            let spawned_thread_id = GetCurrentThreadId();
            println!("Thread id {} started.", spawned_thread_id);

            // Add User APC into queue
            if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
                println!("Add User mode APC into non alertable thread id {}.", spawned_thread_id);
            }
        }

        // Non alertable なスレッドを作成
        non_alertable_func();
    });

    non_alertable_thread_handle.join().unwrap();


    let alertable_thread_handle = thread::spawn(|| {

        unsafe {
            let h_spawned_thread = GetCurrentThread();
            let spawned_thread_id = GetCurrentThreadId();
            println!("Thread id {} started.", spawned_thread_id);

            // Add User APC into queue
            if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
                println!("Add User mode APC into alertable thread id {}.", spawned_thread_id);
            }
        }

        // Alertable なスレッドを作成
        alertable_func();
    });

    alertable_thread_handle.join().unwrap();
    
    return;

}

Loading modules

This time I wanted to use the four API functions GetCurrentThread, GetCurrentThreadId, QueueUserAPC, and SleepEx, so I added windows::Win32::System::Threading, which includes all of them, as a dependency.

windows = { version = "0.60.0", features = [
    "Win32_System_Threading"
] }

Reference: windows::Win32::System::Threading - Rust

Also, because I use thread and time from Rust’s standard library, I load the following modules.

use std::{thread, time};
use windows::Win32::System::Threading::{GetCurrentThread, GetCurrentThreadId, QueueUserAPC, SleepEx};

Implementing alertable functions

Next, I define two functions: non_alertable_func and alertable_func.

Each of these functions is executed on a separate thread via thread::spawn.

fn non_alertable_func() {

    let sleep_time: time::Duration = time::Duration::from_millis(5000);
    let now = time::Instant::now();
    
    println!("Non alertable sleep started.");
    thread::sleep(sleep_time);
    assert!(now.elapsed() >= sleep_time);
    println!("Non alertable sleep started.");

    return;
}


fn alertable_func() {

    let now = time::Instant::now();
    
    // SleepEx による Alertable な Sleep
    println!("Alertable sleep started.");
    unsafe {
        SleepEx(9999999, true);
    }
    // APC がコールされて SleepEx が中断されるので実際には長時間の Sleep は発生しない
    println!("Alertable sleep fineshed in {} nanos.", now.elapsed().as_nanos());

    return;
}

non_alertable_func uses the standard library’s thread::sleep(sleep_time); to sleep in a non-alertable way.

By contrast, alertable_func performs its sleep with the Windows API SleepEx, which causes the thread to transition into an Alertable state.

Defining the APC function

The following is the definition of the APC function that I want to execute via an APC.

Because it is used as an argument to the Windows API, I use extern "system".

extern "system" fn apc_func(_: usize)  {
    println!("User mode APC triggered !!");
    return;
}

I also set the argument to match the definition of the PAPCFUNC type used as an argument to QueueUserAPC.

image-20250408213430392

Reference: PAPCFUNC in windows::Win32::Foundation - Rust

Creating the thread and registering the APC function

Finally, I create the thread with Rust’s thread::spawn and register the APC function with QueueUserAPC.

This time I also wanted more Rust practice, so I tried creating a thread using something other than the Windows API CreateThread.

let alertable_thread_handle = thread::spawn(|| {

    unsafe {
        let h_spawned_thread = GetCurrentThread();
        let spawned_thread_id = GetCurrentThreadId();
        println!("Thread id {} started.", spawned_thread_id);

        // Add User APC into queue
        if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
            println!("Add User mode APC into alertable thread id {}.", spawned_thread_id);
        }
    }

    // Alertable なスレッドを作成
    alertable_func();
});

alertable_thread_handle.join().unwrap();

A thread created by spawn in Rust’s thread module returns an object called JoinHandle, so it seems that you can wait for the thread to finish by calling .join() on it.

Because this JoinHandle is different from a Windows thread handle, I call the GetCurrentThread API inside the thread to obtain the thread handle needed to register the APC function with QueueUserAPC.

Finally, I register the APC by calling QueueUserAPC with the APC function and the thread handle as arguments.

At this point, the first argument to QueueUserAPC needs an Option-wrapped PAPCFUNC, so I wrap the apc_func function defined in the previous section in Some. (I still do not fully understand the concept of the Option enum and Some…)

image-20250408213221842

Reference: QueueUserAPC in windows::Win32::System::Threading - Rust

Analyzing the Rust binary

I had intended to analyze the binary I built at the same time as implementing the Windows API in Rust, but I completely forgot, so I will start doing that from this article onward.

Finding the main function

For now, when I decompiled without a PDB, the main function was output as follows (it clearly is not the actual implementation of main…).

image-20250409002040916

In reality, the function loaded into rcx by lea seems to be the actual main function, and the function called with that as an argument (0x2a90) appears to be the lang_start function.

Reference: Who calls lang_start? - community - The Rust Programming Language Forum

When I actually checked that function, there were places that looked like they used conditional branches and the unwrap function, so it seems reasonable to treat it as the main function.

image-20250409002637075

By the way, Binary Ninja has a pseudo-Rust decompiler mode, so I used it this time, but aside from replacing every variable declaration with let, I did not really feel much difference from the pseudo-C output.

Creating the thread

If you check the function executed at the very beginning of the decompiled result, you notice that the text failed to spawn thread is embedded in it.

image-20250409002942619

This text is likely part of the implementation of the spawn function used to create the thread.

image-20250409003114648

Reference: mod.rs - source

Honestly, it seemed impossible to determine from any other information that this function was thread::spawn. (Even looking at the disassembly results and the decompiled output, I could not make sense of it.)

If I assume that up front, then if this function is thread::spawn, I can conclude that var_90 stores a JoinHandle.

image-20250409003910439

That means this function, which takes a JoinHandle as an argument, is probably non_alertable_thread_handle.join().unwrap() here.

More precisely, because the string embedded when this function’s return value is not 0 is the error text used when panic! is called inside unwrapcalled Result::unwrap() on an Err—it seems likely that the function above corresponds to join() up to that point, and that the contents of the following branch are unwrap().

image-20250409004043509

Incidentally, the code that should have been executed inside the thread created at this point was not included in the analysis results up to here.

When I looked into where it was executed, I found that it was embedded among the objects referenced further down from the first call inside the following function that looked like spawn.

image-20250409002942619

However, understanding the details around this area was still difficult even when checking a binary with symbols, so I gave up for the time being.

It probably means I need to read the implementation of Rust’s modules themselves.

Summary

This time, I wrote code in Rust that creates a thread and executes an arbitrary function with QueueUserAPC.

I also tried analyzing the Rust binary with a decompiler, but even with symbols there were many parts that I could not make sense of, so my reaction was basically, “How are you supposed to analyze this?”

I suspect that if I were more familiar with the implementation of Rust’s modules themselves, there would be a bit more room for informed guessing.

Going forward, I would like to build up more know-how for analyzing Rust.