current commit
261018.1 · chore: update swc to v1.16.18
use std::{
    env,
    ffi::{c_void, OsString},
    fs::{self, File, OpenOptions},
    io,
    os::windows::{
        ffi::{OsStrExt, OsStringExt},
        fs::{MetadataExt, OpenOptionsExt},
    },
    path::{Path, PathBuf},
    ptr,
};

use windows_sys::Win32::{
    Foundation::{CloseHandle, LocalFree, ERROR_ALREADY_EXISTS, ERROR_LOCK_VIOLATION, HANDLE},
    Security::{
        Authorization::{
            ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
            GetNamedSecurityInfoW, GetSecurityInfo, SDDL_REVISION_1, SE_FILE_OBJECT,
        },
        GetAce, GetTokenInformation, TokenUser, ACL, OWNER_SECURITY_INFORMATION,
        SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
    },
    Storage::FileSystem::{
        CreateDirectoryW, GetFileInformationByHandle, LockFileEx, MoveFileExW,
        BY_HANDLE_FILE_INFORMATION, COMPRESSION_FORMAT_NONE, FILE_ATTRIBUTE_COMPRESSED,
        FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE,
        FILE_SHARE_READ, FILE_SHARE_WRITE, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
        MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
    },
    System::{
        Ioctl::FSCTL_SET_COMPRESSION,
        LibraryLoader::{
            GetModuleFileNameW, GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
            GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
        },
        Threading::{GetCurrentProcess, OpenProcessToken},
        IO::{DeviceIoControl, OVERLAPPED},
    },
};

use crate::{Error, ErrorKind, Result};

const SYSTEM_SID: &str = "S-1-5-18";
const ADMINISTRATORS_SID: &str = "S-1-5-32-544";
// Windows system directories may be owned by its servicing account. This trust
// applies only to ancestors: cache directories/files still require our own SID.
const TRUSTED_INSTALLER_SID: &str =
    "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464";
const INHERIT_ONLY_ACE: u8 = 0x08;
const ACCESS_ALLOWED_ACE_TYPE: u8 = 0;
const ACCESS_DENIED_ACE_TYPE: u8 = 1;
const DANGEROUS_DIRECTORY_ACCESS: u32 =
    0x0000_0040 | 0x0001_0000 | 0x0004_0000 | 0x0008_0000 | 0x1000_0000 | 0x4000_0000;

fn wide(path: &Path) -> Vec<u16> {
    path.as_os_str().encode_wide().chain(Some(0)).collect()
}

struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
    fn drop(&mut self) {
        unsafe {
            CloseHandle(self.0);
        }
    }
}

struct LocalAllocation(*mut c_void);
impl Drop for LocalAllocation {
    fn drop(&mut self) {
        unsafe {
            LocalFree(self.0);
        }
    }
}

fn sid_string(sid: *mut c_void) -> io::Result<String> {
    let mut text = ptr::null_mut();
    if unsafe { ConvertSidToStringSidW(sid, &mut text) } == 0 {
        return Err(io::Error::last_os_error());
    }
    let _allocation = LocalAllocation(text.cast());
    let mut len = 0;
    unsafe {
        while *text.add(len) != 0 {
            len += 1;
        }
        Ok(String::from_utf16_lossy(std::slice::from_raw_parts(
            text, len,
        )))
    }
}

fn current_sid() -> io::Result<String> {
    let mut token = 0;
    if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
        return Err(io::Error::last_os_error());
    }
    let token = OwnedHandle(token);
    let mut size = 0;
    unsafe {
        GetTokenInformation(token.0, TokenUser, ptr::null_mut(), 0, &mut size);
    }
    // TOKEN_USER contains pointers and needs pointer alignment, not Vec<u8>'s
    // nominal alignment. The trailing SID remains inside this allocation.
    let mut storage = vec![0_usize; (size as usize).div_ceil(std::mem::size_of::<usize>())];
    if unsafe {
        GetTokenInformation(
            token.0,
            TokenUser,
            storage.as_mut_ptr().cast(),
            size,
            &mut size,
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    sid_string(unsafe { (*(storage.as_ptr().cast::<TOKEN_USER>())).User.Sid })
}

pub fn user_namespace() -> Result<String> {
    current_sid()
        .map(|sid| format!("swc-native-{sid}"))
        .map_err(|e| Error::io(ErrorKind::Cache, "identify cache owner", e))
}

pub fn user_cache_root() -> Result<PathBuf> {
    env::var_os("LOCALAPPDATA")
        .filter(|value| Path::new(value).is_absolute())
        .map(|value| PathBuf::from(value).join("swc"))
        .ok_or_else(|| {
            Error::new(
                ErrorKind::Cache,
                "cannot determine native addon user cache: LOCALAPPDATA must be an absolute \
                 directory",
            )
        })
}

/// AppContainer grants inherited by LocalAppData can make it unsafe for native
/// loading. Use a separately validated profile cache rather than trusting those
/// grants or changing the user's directory ACLs.
pub(crate) fn fallback_cache_root() -> Result<PathBuf> {
    env::var_os("USERPROFILE")
        .filter(|value| Path::new(value).is_absolute())
        .map(|value| PathBuf::from(value).join(".swc-cache"))
        .ok_or_else(|| {
            Error::new(
                ErrorKind::Cache,
                "cannot determine native addon profile cache: USERPROFILE must be an absolute \
                 directory",
            )
        })
}

/// Windows does not expose a mount-level `noexec` equivalent for ordinary
/// addon cache paths. Filesystem access is validated during cache creation.
pub fn executable_cache_root(_root: &Path) -> Result<()> {
    Ok(())
}

pub fn secure_cache_root(root: &Path) -> io::Result<()> {
    if !root.is_absolute() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "cache root must be absolute",
        ));
    }
    let sid = current_sid()?;
    // A reparse point or an untrusted owner/DACL in any component can redirect
    // or replace the validated cache namespace before LoadLibrary reopens it.
    for directory in root.ancestors() {
        validate_cache_ancestor(directory, &sid)?;
    }
    Ok(())
}

fn validate_cache_ancestor(path: &Path, current_sid: &str) -> io::Result<()> {
    let metadata = fs::symlink_metadata(path)?;
    if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!(
                "cache ancestor {} is a non-directory or reparse point",
                path.display()
            ),
        ));
    }

    use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;

    let mut owner = ptr::null_mut();
    let mut dacl = ptr::null_mut();
    let mut security = ptr::null_mut();
    let status = unsafe {
        GetNamedSecurityInfoW(
            wide(path).as_ptr(),
            SE_FILE_OBJECT,
            OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
            &mut owner,
            ptr::null_mut(),
            &mut dacl,
            ptr::null_mut(),
            &mut security,
        )
    };
    if status != 0 {
        return Err(io::Error::from_raw_os_error(status as i32));
    }
    let _security = LocalAllocation(security);
    let owner = sid_string(owner)?;
    validate_ancestor_security(path, &owner, dacl, current_sid)
}

fn validate_ancestor_security(
    path: &Path,
    owner: &str,
    dacl: *mut ACL,
    current_sid: &str,
) -> io::Result<()> {
    if !trusted_principal(owner, current_sid) {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!(
                "cache ancestor {} has untrusted owner SID {owner}",
                path.display()
            ),
        ));
    }
    validate_ancestor_dacl(dacl, current_sid).map_err(|error| {
        io::Error::new(
            error.kind(),
            format!("cache ancestor {}: {error}", path.display()),
        )
    })
}

fn trusted_principal(sid: &str, current_sid: &str) -> bool {
    sid == current_sid
        || sid == SYSTEM_SID
        || sid == ADMINISTRATORS_SID
        || sid == TRUSTED_INSTALLER_SID
}

/// Conservatively reject ACLs that grant an untrusted principal the rights to
/// remove, retake ownership of, or rewrite a cache-root ancestor. This avoids
/// mutating caller-owned roots while preventing pathname substitution.
fn validate_ancestor_dacl(dacl: *mut ACL, current_sid: &str) -> io::Result<()> {
    let denied = |message| io::Error::new(io::ErrorKind::PermissionDenied, message);
    if dacl.is_null() {
        // A null DACL grants full access to every account.
        return Err(denied(
            "null DACL grants replacement to everyone".to_owned(),
        ));
    }
    for index in 0..u32::from(unsafe { (*dacl).AceCount }) {
        let mut ace = ptr::null_mut();
        if unsafe { GetAce(dacl, index, &mut ace) } == 0 {
            return Err(io::Error::last_os_error());
        }
        let flags = unsafe { *ace.cast::<u8>().add(1) };
        // An inheritance-only grant applies to children, not this directory.
        // Its inherited effective copy is checked when visiting that child.
        if flags & INHERIT_ONLY_ACE != 0 {
            continue;
        }
        let ace_type = unsafe { *ace.cast::<u8>() };
        if ace_type == ACCESS_DENIED_ACE_TYPE {
            continue;
        }
        if ace_type != ACCESS_ALLOWED_ACE_TYPE {
            // Object and callback allow ACEs use a different SID offset. Do
            // not risk misclassifying an untrusted grant as harmless.
            return Err(denied(format!("unsupported effective ACE type {ace_type}")));
        }
        let mask = unsafe { std::ptr::read_unaligned(ace.cast::<u8>().add(4).cast::<u32>()) };
        if mask & DANGEROUS_DIRECTORY_ACCESS == 0 {
            continue;
        }
        // ACCESS_ALLOWED_ACE stores the SID immediately after its header and
        // access mask (eight bytes in total).
        let trustee = sid_string(unsafe { ace.cast::<u8>().add(8).cast() })?;
        if !trusted_principal(&trustee, current_sid) {
            return Err(denied(format!(
                "DACL grants replacement rights {mask:#x} to SID {trustee}"
            )));
        }
    }
    Ok(())
}

pub fn private_directory(path: &Path) -> io::Result<()> {
    let sid = current_sid()?;
    let sddl: Vec<u16> = format!("O:{sid}D:P(A;;FA;;;{sid})(A;;FA;;;SY)")
        .encode_utf16()
        .chain(Some(0))
        .collect();
    let mut descriptor = ptr::null_mut();
    if unsafe {
        ConvertStringSecurityDescriptorToSecurityDescriptorW(
            sddl.as_ptr(),
            SDDL_REVISION_1,
            &mut descriptor,
            ptr::null_mut(),
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    let _descriptor = LocalAllocation(descriptor);
    let attributes = SECURITY_ATTRIBUTES {
        nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
        lpSecurityDescriptor: descriptor,
        bInheritHandle: 0,
    };
    if unsafe { CreateDirectoryW(wide(path).as_ptr(), &attributes) } == 0 {
        let error = io::Error::last_os_error();
        if error.raw_os_error() != Some(ERROR_ALREADY_EXISTS as i32) {
            return Err(error);
        }
    }
    let metadata = fs::symlink_metadata(path)?;
    if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "cache directory must not be a reparse point",
        ));
    }
    let mut owner = ptr::null_mut();
    let mut security = ptr::null_mut();
    let status = unsafe {
        GetNamedSecurityInfoW(
            wide(path).as_ptr(),
            SE_FILE_OBJECT,
            OWNER_SECURITY_INFORMATION,
            &mut owner,
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            &mut security,
        )
    };
    if status != 0 {
        return Err(io::Error::from_raw_os_error(status as i32));
    }
    let _security = LocalAllocation(security);
    if sid_string(owner)? != sid {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "cache directory belongs to another user",
        ));
    }
    // An existing user-owned directory may have inherited a permissive DACL.
    // Restore the protected owner/SYSTEM DACL before using any cache entries.
    use windows_sys::Win32::Security::{
        SetFileSecurityW, DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION,
    };
    if unsafe {
        SetFileSecurityW(
            wide(path).as_ptr(),
            DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
            descriptor,
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

/// An elevated token can default new files to the Administrators owner. Never
/// weaken validation to accept that owner: create our own files with the user's
/// SID explicitly, and leave existing files subject to the original checks.
pub(crate) fn new_private_file(path: &Path) -> io::Result<File> {
    use std::os::windows::io::FromRawHandle;

    use windows_sys::Win32::{
        Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE},
        Storage::FileSystem::{CreateFileW, CREATE_NEW, FILE_ATTRIBUTE_NORMAL},
    };
    let sid = current_sid()?;
    let sddl: Vec<u16> = format!("O:{sid}D:P(A;;FA;;;{sid})(A;;FA;;;SY)")
        .encode_utf16()
        .chain(Some(0))
        .collect();
    let mut descriptor = ptr::null_mut();
    if unsafe {
        ConvertStringSecurityDescriptorToSecurityDescriptorW(
            sddl.as_ptr(),
            SDDL_REVISION_1,
            &mut descriptor,
            ptr::null_mut(),
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    let _descriptor = LocalAllocation(descriptor);
    let attributes = SECURITY_ATTRIBUTES {
        nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
        lpSecurityDescriptor: descriptor,
        bInheritHandle: 0,
    };
    let handle = unsafe {
        CreateFileW(
            wide(path).as_ptr(),
            GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
            &attributes,
            CREATE_NEW,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT,
            0,
        )
    };
    if handle == INVALID_HANDLE_VALUE {
        return Err(io::Error::last_os_error());
    }
    Ok(unsafe { File::from_raw_handle(handle as _) })
}

pub fn open_regular(path: &Path, write: bool, create: bool) -> io::Result<File> {
    use std::os::windows::io::AsRawHandle;
    let open = || {
        OpenOptions::new()
            .read(true)
            .write(write)
            .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
            .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
            .open(path)
    };
    let file = if create {
        match new_private_file(path) {
            Ok(file) => file,
            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => open()?,
            Err(error) => return Err(error),
        }
    } else {
        open()?
    };
    let metadata = file.metadata()?;
    if !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "cache entry must be a regular file without a reparse point",
        ));
    }
    let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
    if unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) } == 0 {
        return Err(io::Error::last_os_error());
    }
    if info.nNumberOfLinks != 1 {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "cache entry must not be hardlinked",
        ));
    }
    let mut owner = ptr::null_mut();
    let mut security = ptr::null_mut();
    let status = unsafe {
        GetSecurityInfo(
            file.as_raw_handle() as HANDLE,
            SE_FILE_OBJECT,
            OWNER_SECURITY_INFORMATION,
            &mut owner,
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            &mut security,
        )
    };
    if status != 0 {
        return Err(io::Error::from_raw_os_error(status as i32));
    }
    let _security = LocalAllocation(security);
    let sid = current_sid()?;
    let owner = sid_string(owner)?;
    if owner != sid {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!(
                "cache entry {} has owner SID {owner}, expected {sid}",
                path.display()
            ),
        ));
    }
    protect_entry_dacl(path, &sid)?;
    Ok(file)
}

/// Remove inherited grants before verifying cache bytes or allowing the image
/// loader to reopen the entry by pathname.
fn protect_entry_dacl(path: &Path, sid: &str) -> io::Result<()> {
    use windows_sys::Win32::Security::{
        SetFileSecurityW, DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION,
    };

    let sddl: Vec<u16> = format!("O:{sid}D:P(A;;FA;;;{sid})(A;;FA;;;SY)")
        .encode_utf16()
        .chain(Some(0))
        .collect();
    let mut descriptor = ptr::null_mut();
    if unsafe {
        ConvertStringSecurityDescriptorToSecurityDescriptorW(
            sddl.as_ptr(),
            SDDL_REVISION_1,
            &mut descriptor,
            ptr::null_mut(),
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    let _descriptor = LocalAllocation(descriptor);
    if unsafe {
        SetFileSecurityW(
            wide(path).as_ptr(),
            DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
            descriptor,
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

pub fn create_cache_root(path: &Path) -> io::Result<()> {
    fs::create_dir_all(path)
}

pub fn lock_exclusive(file: &File) -> io::Result<()> {
    use std::os::windows::io::AsRawHandle;
    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
    if unsafe {
        LockFileEx(
            file.as_raw_handle() as HANDLE,
            LOCKFILE_EXCLUSIVE_LOCK,
            0,
            1,
            0,
            &mut overlapped,
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

/// Try to acquire an exclusive lock without waiting for an active loader.
pub fn try_lock_exclusive(file: &File) -> io::Result<bool> {
    use std::os::windows::io::AsRawHandle;

    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
    if unsafe {
        LockFileEx(
            file.as_raw_handle() as HANDLE,
            LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
            0,
            1,
            0,
            &mut overlapped,
        )
    } != 0
    {
        return Ok(true);
    }
    let error = io::Error::last_os_error();
    if error.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32) {
        Ok(false)
    } else {
        Err(error)
    }
}

pub fn sync_directory(_path: &Path) -> io::Result<()> {
    // Windows does not expose POSIX directory fsync. All file contents are
    // flushed before the same-volume atomic rename; no partial file is visible.
    Ok(())
}

/// Replace an existing payload on the same volume. Unlike `std::fs::rename`,
/// MoveFileEx supports replacement on Windows.
pub fn replace_file(source: &Path, destination: &Path) -> io::Result<()> {
    if unsafe {
        MoveFileExW(
            wide(source).as_ptr(),
            wide(destination).as_ptr(),
            MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

/// # Safety
/// `address` must point into a currently loaded native image.
pub unsafe fn carrier_path(address: *const c_void) -> Result<PathBuf> {
    let mut module = 0;
    if GetModuleHandleExW(
        GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
        address.cast(),
        &mut module,
    ) == 0
    {
        return Err(Error::io(
            ErrorKind::Load,
            "locate native carrier module",
            io::Error::last_os_error(),
        ));
    }
    let mut buffer = vec![0; 256];
    loop {
        let len = GetModuleFileNameW(module, buffer.as_mut_ptr(), buffer.len() as u32) as usize;
        if len == 0 {
            return Err(Error::io(
                ErrorKind::Load,
                "resolve native carrier path",
                io::Error::last_os_error(),
            ));
        }
        if len < buffer.len() {
            return Ok(PathBuf::from(OsString::from_wide(&buffer[..len])));
        }
        if buffer.len() >= 32768 {
            return Err(Error::new(
                ErrorKind::Load,
                "native carrier path exceeds Windows path limit",
            ));
        }
        buffer.resize((buffer.len() * 2).min(32768), 0);
    }
}

/// Prepare a new, empty image before decoding. NTFS compression can be
/// inherited from a user-selected cache directory, even when we never request
/// it ourselves. Clear it through the existing private write handle: first
/// loading compressed DLLs is substantially slower. Published images are never
/// modified in place.
pub(crate) fn prepare_cache_image(file: &File) -> io::Result<()> {
    use std::os::windows::io::AsRawHandle;
    if file.metadata()?.file_attributes() & FILE_ATTRIBUTE_COMPRESSED == 0 {
        return Ok(());
    }
    let mut format = COMPRESSION_FORMAT_NONE;
    let mut returned = 0;
    if unsafe {
        DeviceIoControl(
            file.as_raw_handle() as HANDLE,
            FSCTL_SET_COMPRESSION,
            (&mut format as *mut u16).cast(),
            2,
            ptr::null_mut(),
            0,
            &mut returned,
            ptr::null_mut(),
        )
    } == 0
    {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

#[cfg(test)]
#[path = "windows_tests.rs"]
mod tests;