Need working Rust code for creating/editing files on ESP (EFI System Partition) in Windows

Hello everyone!

I am writing a utility in Rust for creating/editing files on the EFI System Partition (ESP) under Windows (creating/editing a file on the FAT32 ESP partition). Reading and backing up work fine, but when I try to write, I run into Windows system protection: I get ERROR_ACCESS_DENIED (code 5) when writing via .\PhysicalDriveN, even with administrator rights.

What has already been done:
• ESP search via GPT has been implemented in Rust:
• I open .\PhysicalDriveN, read GPT (EFI PART), and search for a partition of type C12A7328-F81F-11D2-BA4B-00A0C93EC93B.
• I calculate start_lba and length_lba, read ESP into memory (works stably).
• Implemented volume enumeration (FindFirstVolumeW/FindNextVolumeW) and mapping via IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS:
• Find the volume whose start_lba/length_lba match the ESP on the desired disk.

Tried:
• Locking and unmounting the volume (FSCTL_LOCK_VOLUME/FSCTL_DISMOUNT_VOLUME) — on the volume descriptor; Windows returns ERROR_ACCESS_DENIED.
• Write directly to .\PhysicalDriveN with FILE_FLAG_NO_BUFFERING|WRITE_THROUGH, aligned to sector size — ERROR_ACCESS_DENIED on WriteFile.
• Opening the physical disk with GENERIC_ALL does not help, it falls back to GENERIC_READ|GENERIC_WRITE, writing is still prohibited.
• Dry-run mode (all FAT32 logic in memory) is fully functional: I create/edit a file, correctly update FAT/directories, etc.

Log/symptoms (abbreviated):
• ESP: PhysicalDrive0, start_lba=2048, length_lba=204800, 512 B/sector.
• ESP volume is located correctly: ?\Volume{...}, start_lba=2048, length_lba=204800.
• FSCTL_LOCK_VOLUME on the volume returns 5 (Access denied).
• Writing to .\PhysicalDrive0 at offset start_lba*sector_size — WriteFile -> 5 (Access denied).

Environment:
• Windows 11 x64, running with administrator privileges.
• Rust: 1.78+ (stable), windows library for WinAPI.

Why working “through the file system” is not suitable:
• mountvol X: /s is not always available/reliable in my environment. I need a method that is guaranteed to work in headless/script mode. But if there is a reliable program through the file system without raw I/O, I will also consider it.

I would be very grateful for:
• Rust code snippets/mini-project that you actually write in ESP on Windows 10/11.
• A confirmed working sequence of API calls (FSCTL/IOCTL/privileges/mount).

Hey! I am no Windows API expert, and don't use Windows - but tackling the complexity is appealing to me, so I like a Windows API challenge :). I tested the below in VM.

Why working “through the file system” is not suitable:
• mountvol X: /s is not always available/reliable in my environment. I need a method that is guaranteed to work in headless/script mode. But if there is a reliable program through the file system without raw I/O, I will also consider it.

You don't need to call mountvol - why not use the Windows API to access the files directly? In Windows, you don't even need to "mount", and you can use the standard Rust file APIs, and treat the \\?\Volume{GUID}\ path as just a folder. This is surely a lot simpler and more reliable with a lower chance of edge cases etc.

I think you are overthinking things a bit. All you need to do is from FindFirstVolumeW/FindNextVolumeW is call DeviceIoControl on each partition with IOCTL_DISK_GET_PARTITION_INFO_EX, then if it is EFI type (the GUID is defined as PARTITION_SYSTEM_GUID), save it.

This is the rust code I came up with (96 lines, depending only on std, windows and scopeguard):

use std::{
    error::Error,
    ffi::{OsString, c_void},
    fs::{File, read_to_string},
    io::Write,
    os::windows::ffi::OsStringExt,
    path::PathBuf,
};

use scopeguard::defer;
use windows::Win32::{
    Foundation::CloseHandle,
    Storage::FileSystem::{
        CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
        FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, OPEN_EXISTING, PARTITION_SYSTEM_GUID,
    },
    System::{
        IO::DeviceIoControl,
        Ioctl::{IOCTL_DISK_GET_PARTITION_INFO_EX, PARTITION_INFORMATION_EX, PARTITION_STYLE_GPT},
    },
};
use windows_strings::PCWSTR;

fn main() -> Result<(), Box<dyn Error>> {
    // 50 = \\?\Volume{<guid>}\NUL
    let mut path = [0u16; 50];

    let find_handle = unsafe { FindFirstVolumeW(&mut path) }?;

    defer! { unsafe {
        let _ = FindVolumeClose(find_handle);
    } };

    loop {
        // Remove trailing \ as required for
        path[48] = 0;
        // Open the volume as a handle:
        let volume_handle = unsafe {
            CreateFileW(
                PCWSTR(path.as_ptr()),
                0,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                None,
                OPEN_EXISTING,
                FILE_ATTRIBUTE_NORMAL,
                None,
            )
        }?;

        defer! { unsafe {
            let _ = CloseHandle(volume_handle);
        } };

        let mut information = PARTITION_INFORMATION_EX::default();

        let mut bytes_returned = 0;

        unsafe {
            DeviceIoControl(
                volume_handle,
                IOCTL_DISK_GET_PARTITION_INFO_EX,
                None,
                0,
                Some((&mut information) as *mut PARTITION_INFORMATION_EX as *mut c_void),
                size_of::<PARTITION_INFORMATION_EX>() as u32,
                Some(&mut bytes_returned),
                None,
            )
        }?;

        assert_eq!(bytes_returned, size_of::<PARTITION_INFORMATION_EX>() as u32);

        if information.PartitionStyle == PARTITION_STYLE_GPT
            && unsafe { information.Anonymous.Gpt }.PartitionType == PARTITION_SYSTEM_GUID
        {
            break;
        }

        unsafe { FindNextVolumeW(find_handle, &mut path) }?;
    }

    let path = PathBuf::from(OsString::from_wide(&path[0..48]));

    let test_path = path.join("test.txt");

    {
        let mut test_file = File::create(&test_path)?;
        test_file.write_all(b"Hello, world!")?;
    }

    assert_eq!(read_to_string(test_path)?, "Hello, world!");

    println!("No errors!");

    Ok(())
}

Obviously, this code assumes only 1 EFI partition exists, and that EFI partitions exist if and only if the system is GPT (which is not technically true, but what Windows assumes).

Also, note that IOCTL_DISK_GET_PARTITION_INFO_EX might fail:

The IOCTL_DISK_GET_PARTITION_INFO_EX control code is supported on basic disks. It is only supported on dynamic disks that are boot or system disks.

In practice, an EFI System Partition will only ever be on a basic disk or boot/system disk. However, you might want to ignore whichever error is returned. If you want to filter to only certain physical disks, you can use IOCTL_STORAGE_GET_DEVICE_NUMBER and check accordingly.

1 Like

If you want to make sure you get the current ESP that was used to boot the current OS, here is another way - you can use the undocumented SystemFirmwarePartitionInformation class to NtQuerySystemInformation. From there, you can read buf as SYSTEM_FIRMWARE_PARTITION_INFORMATION, get the .FirmwarePartition. That gets you a physical device name - one that looks like \Device\HarddiskVolume1. Then, with enough trickery, you can convert to the path:

use std::{
    error::Error,
    ffi::{c_ulong, c_void},
    mem::offset_of,
    ptr::null_mut,
};

use scopeguard::defer;
use windows::{
    Wdk::System::SystemInformation::{NtQuerySystemInformation, SYSTEM_INFORMATION_CLASS},
    Win32::{
        Foundation::{CloseHandle, GENERIC_READ, MAX_PATH, UNICODE_STRING},
        Storage::FileSystem::{CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, OPEN_EXISTING},
        System::{IO::DeviceIoControl, Ioctl::FILE_ANY_ACCESS},
    },
};
use windows_strings::w;

#[derive(Default)]
#[repr(C)]
pub struct SYSTEM_FIRMWARE_PARTITION_INFORMATION {
    pub FirmwarePartition: UNICODE_STRING,
}

pub const SystemFirmwarePartitionInformation: SYSTEM_INFORMATION_CLASS =
    SYSTEM_INFORMATION_CLASS(200);

pub const MOUNTMGRCONTROLTYPE: c_ulong = 'm' as u32;

pub const fn CTL_CODE(
    device_type: c_ulong,
    function: c_ulong,
    method: c_ulong,
    access: c_ulong,
) -> c_ulong {
    return (device_type << 16) | (access << 14) | (function << 2) | method;
}

pub const IOCTL_MOUNTMGR_QUERY_POINTS: c_ulong =
    CTL_CODE(MOUNTMGRCONTROLTYPE, 2, METHOD_BUFFERED, FILE_ANY_ACCESS);

#[derive(Default)]
#[repr(C)]
pub struct MOUNTMGR_MOUNT_POINT {
    pub SymbolicLinkNameOffset: u32,
    pub SymbolicLinkNameLength: u16,
    pub Reserved1: u16,
    pub UniqueIdOffset: u32,
    pub UniqueIdLength: u16,
    pub Reserved2: u16,
    pub DeviceNameOffset: u32,
    pub DeviceNameLength: u16,
    pub Reserved3: u16,
}

#[derive(Default)]
#[repr(C)]
pub struct MOUNTMGR_MOUNT_POINTS {
    pub Size: u32,
    pub NumberOfMountPoints: u32,
    pub MountPoints: [MOUNTMGR_MOUNT_POINT; 1],
}

fn main() -> Result<(), Box<dyn Error>> {
    let mut buf = vec![0u8; size_of::<SYSTEM_FIRMWARE_PARTITION_INFORMATION>() + MAX_PATH as usize];

    unsafe {
        NtQuerySystemInformation(
            SystemFirmwarePartitionInformation,
            buf.as_mut_ptr() as *mut c_void,
            MAX_PATH,
            null_mut(),
        )
    }
    .ok()?;

    let part_info: SYSTEM_FIRMWARE_PARTITION_INFORMATION = unsafe {
        buf.as_ptr()
            .cast::<SYSTEM_FIRMWARE_PARTITION_INFORMATION>()
            .read()
    };

    let path_slice_u8 = unsafe {
        std::slice::from_raw_parts(
            part_info.FirmwarePartition.Buffer.0 as *const u8,
            part_info.FirmwarePartition.Length as usize,
        )
    };

    let mountmgr_handle = unsafe {
        CreateFileW(
            w!("\\\\.\\MountPointManager"),
            GENERIC_READ.0,
            FILE_SHARE_READ,
            None,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            None,
        )
    }?;

    defer! {
        let _ = unsafe { CloseHandle(mountmgr_handle) };
    }

    let input = MOUNTMGR_MOUNT_POINT::default();

    let mut input_buf = vec![0u8; size_of::<MOUNTMGR_MOUNT_POINT>() + path_slice_u8.len()];
    let hdr_sz = size_of::<MOUNTMGR_MOUNT_POINT>();

    {
        let input_mount_point: *mut MOUNTMGR_MOUNT_POINT = input_buf.as_mut_ptr().cast();
        unsafe {
            (*input_mount_point).DeviceNameOffset = hdr_sz as u32;
            (*input_mount_point).DeviceNameLength = path_slice_u8.len() as u16;
        }
    }

    (&mut input_buf[hdr_sz..]).copy_from_slice(path_slice_u8);

    let mut out = vec![0u8; 64 * 1024];
    let mut bytes = 0u32;

    unsafe {
        DeviceIoControl(
            mountmgr_handle,
            IOCTL_MOUNTMGR_QUERY_POINTS,
            Some(input_buf.as_ptr().cast()),
            input_buf.len() as u32,
            Some(out.as_mut_ptr().cast()),
            out.len() as u32,
            Some(&mut bytes),
            None,
        )
    }?;

    let out_mounts: MOUNTMGR_MOUNT_POINTS =
        unsafe { out.as_ptr().cast::<MOUNTMGR_MOUNT_POINTS>().read() };

    let mp_offset = offset_of!(MOUNTMGR_MOUNT_POINTS, MountPoints);

    for mountpoint in unsafe {
        std::slice::from_raw_parts(
            out.as_ptr()
                .byte_add(mp_offset)
                .cast::<MOUNTMGR_MOUNT_POINT>(),
            out_mounts.NumberOfMountPoints as usize,
        )
    } {
        if mountpoint.SymbolicLinkNameOffset == 0 || mountpoint.SymbolicLinkNameLength == 0 {
            continue;
        }

        let name = unsafe {
            std::slice::from_raw_parts(
                out.as_ptr()
                    .byte_add(mountpoint.SymbolicLinkNameOffset as usize)
                    .cast::<u16>(),
                mountpoint.SymbolicLinkNameLength as usize / 2,
            )
        };

        // I am just using regular string here, but you could be more efficient.
        let name = String::from_utf16(name)?;

        if name.starts_with(r"\??\Volume{") && name.ends_with('}') {
            // Convert to Win32: \\?\Volume{GUID}\
            let name = format!(r"\\?\{}\", name.trim_start_matches(r"\??\")); // -> \\?\Volume{GUID}
            println!("{name}"); // append trailing backslash
        }
    }

    println!("No errors!");

    Ok(())
}

Of course, as with all undocumented APIs, here be dragons. IOCTL_MOUNTMGR_QUERY_POINTS however is documented (albeit poorly) - however there is no equivalent in windows-rs.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.