Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ ptr_cast_constness = "warn"
unnecessary_safety_comment = "warn"
missing_safety_doc = "warn"
as_pointer_underscore = "warn"
undocumented_unsafe_blocks = "warn"
multiple_unsafe_ops_per_block = "warn"

[workspace.dependencies]
uuid = { version = "1.18", default-features = false }
Expand Down
17 changes: 9 additions & 8 deletions ffi/src/dpapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ pub unsafe extern "system" fn DpapiUnprotectSecret(
check_null!(username);
check_null!(password);
check_null!(secret);
check_null!(secret_len);

try_execute!(sspi::install_default_crypto_provider_if_necessary().map_err(|_| "failed to initialize default crypto provider"), NTE_INTERNAL_ERROR);

Expand Down Expand Up @@ -422,6 +423,10 @@ pub unsafe extern "system" fn DpapiFree(buf: LpCByte) -> u32 {
}

#[cfg(test)]
#[expect(
clippy::undocumented_unsafe_blocks,
reason = "undocumented unsafe is acceptable in tests"
)]
mod tests {
//! This tests simulate `DpapiProtectSecret`, `DpapiUnprotectSecret`, and `DpapiFree` function calls.
//! It's better to run them using Miri: https://github.com/rust-lang/miri.
Expand Down Expand Up @@ -488,10 +493,8 @@ mod tests {
assert!(!decrypted_secret.is_null());
assert!(secret_len > 0);

unsafe {
DpapiFree(blob);
DpapiFree(decrypted_secret);
}
unsafe { DpapiFree(blob) };
unsafe { DpapiFree(decrypted_secret) };
}

#[test]
Expand Down Expand Up @@ -549,9 +552,7 @@ mod tests {
assert!(!decrypted_secret.is_null());
assert!(secret_len > 0);

unsafe {
DpapiFree(blob);
DpapiFree(decrypted_secret);
}
unsafe { DpapiFree(blob) };
unsafe { DpapiFree(decrypted_secret) };
}
}
2 changes: 0 additions & 2 deletions ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
#![allow(clippy::print_stdout)]
#![allow(non_snake_case)]
#![deny(unsafe_op_in_unsafe_fn)]
#![warn(clippy::undocumented_unsafe_blocks)]

#[macro_use]
extern crate tracing;

#[cfg(feature = "dpapi")]
#[deny(unsafe_op_in_unsafe_fn)]
#[warn(clippy::undocumented_unsafe_blocks)]
pub mod dpapi;
pub mod logging;
pub mod sspi;
Expand Down
8 changes: 7 additions & 1 deletion ffi/src/sspi/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,11 @@ pub unsafe extern "system" fn DeleteSecurityContext(mut ph_context: PCtxtHandle)
)}
);

// SAFETY: `sspi_context_ptr` is a valid, local pointer to the `SspiHandle` allocated by the `p_ctx_handle_to_sspi_context`.
let sspi_context_ptr = unsafe { sspi_context_ptr.as_mut() };
Comment on lines +293 to +294
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m a bit surprised by this because obtaining a pointer is generally not unsafe 🤔

// SAFETY: `sspi_context_ptr` is a valid, local pointer to the `SspiHandle` allocated by the `p_ctx_handle_to_sspi_context`.
let _context: Box<SspiHandle> = unsafe {
Box::from_raw(sspi_context_ptr.as_mut())
Box::from_raw(sspi_context_ptr)
};

// SAFETY:
Expand Down Expand Up @@ -671,6 +673,10 @@ unsafe fn copy_decrypted_buffers(to_buffers: PSecBuffer, from_buffers: Vec<Secur
}

#[cfg(test)]
#[expect(
clippy::undocumented_unsafe_blocks,
reason = "undocumented unsafe is acceptable in tests"
)]
mod tests {
use std::ptr::null_mut;
use std::slice::from_raw_parts;
Expand Down
103 changes: 70 additions & 33 deletions ffi/src/sspi/sec_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,12 +1283,23 @@ pub unsafe extern "system" fn SetCredentialsAttributesA(
0
} else if ul_attribute == SECPKG_CRED_ATTR_KDC_URL {
let cred_attr = p_buffer.cast::<SecPkgCredentialsKdcUrlA>();

// SAFETY:
// - `cred_attr` is guaranteed to be non-null due to the prior check.
let kdc_url = unsafe {
(*cred_attr).kdc_url
};

if kdc_url.is_null() {
return ErrorKind::IncompleteCredentials.to_u32().unwrap();
}

let kdc_url = try_execute!(
// SAFETY:
// - `p_buffer` is guaranteed to be non-null due to the prior check.
// - The memory region `p_buffer` contains a valid null-terminator at the end of string.
// - The memory region `p_buffer` points to is valid for reads of bytes up to and including null-terminator.
unsafe { CStr::from_ptr((*cred_attr).kdc_url) }.to_str(),
// - `kdc_url` is non-null due to the prior check.
// - The memory region `kdc_url` contains a valid null-terminator at the end of string.
// - The memory region `kdc_url` points to is valid for reads of bytes up to and including null-terminator.
unsafe { CStr::from_ptr(kdc_url) }.to_str(),
ErrorKind::InvalidParameter
);
credentials_handle.attributes.kdc_url = Some(kdc_url.to_string());
Expand Down Expand Up @@ -1364,12 +1375,22 @@ pub unsafe extern "system" fn SetCredentialsAttributesW(
0
} else if ul_attribute == SECPKG_CRED_ATTR_KDC_URL {
let cred_attr = p_buffer.cast::<SecPkgCredentialsKdcUrlW>();
// SAFETY:
// - `cred_attr` is guaranteed to be non-null due to the prior check.
let kdc_url = unsafe {
(*cred_attr).kdc_url
}.cast_const();

if kdc_url.is_null() {
return ErrorKind::IncompleteCredentials.to_u32().unwrap();
}

let kdc_url = try_execute!(
// SAFETY:
// - `p_buffer` is guaranteed to be non-null due to the prior check.
// - The memory region `p_buffer` contains a valid null-terminator at the end of string.
// - The memory region `p_buffer` points to is valid for reads of bytes up to and including null-terminator.
unsafe { c_w_str_to_string((*cred_attr).kdc_url.cast_const()) }.map_err(Error::from)
// - `kdc_url` is non-null due to the prior check.
// - The memory region `kdc_url` contains a valid null-terminator at the end of string.
// - The memory region `kdc_url` points to is valid for reads of bytes up to and including null-terminator.
unsafe { c_w_str_to_string(kdc_url) }.map_err(Error::from)
);
credentials_handle.attributes.kdc_url = Some(kdc_url);

Expand Down Expand Up @@ -1455,11 +1476,13 @@ pub unsafe extern "system" fn ChangeAccountPasswordA(

check_null!(p_output.p_buffers);

let mut output_tokens =
// SAFETY:
// - `p_output.p_buffers` is guaranteed to be non-null due to the prior check.
// - The memory region `p_output.p_buffers` points to is valid for reads of `len` elements.
unsafe { p_sec_buffers_to_security_buffers(from_raw_parts(p_output.p_buffers, len)) };
// SAFETY:
// - `p_output.p_buffers` is guaranteed to be non-null due to the prior check.
// - The memory region `p_output.p_buffers` points to is valid for reads of `len` elements.
let p_buffers = unsafe { from_raw_parts(p_output.p_buffers, len) };
// SAFETY:
// - `p_buffers` must contain valid [SecBuffer] structures: upheld by the user.
let mut output_tokens = unsafe { p_sec_buffers_to_security_buffers(p_buffers) };
output_tokens.iter_mut().for_each(|s| s.buffer.clear());

let change_password = ChangePasswordBuilder::new()
Expand Down Expand Up @@ -1683,6 +1706,10 @@ pub extern "system" fn QueryCredentialsAttributesExW(
pub type QueryCredentialsAttributesExFnW = extern "system" fn(PCredHandle, u32, *mut c_void, u32) -> SecurityStatus;

#[cfg(test)]
#[expect(
clippy::undocumented_unsafe_blocks,
reason = "undocumented unsafe is acceptable in tests"
)]
mod tests {
use std::ffi::CStr;
use std::ptr::{self, null, null_mut};
Expand Down Expand Up @@ -1712,20 +1739,23 @@ mod tests {
#[test]
fn initialize_security_context_w() {
let pkg_name = "NTLM\0".encode_utf16().collect::<Vec<_>>();
let mut pkg_info: PSecPkgInfoW = null_mut::<SecPkgInfoW>();
let mut pkg_info_ptr: PSecPkgInfoW = null_mut::<SecPkgInfoW>();

let status = unsafe { QuerySecurityPackageInfoW(pkg_name.as_ptr(), &mut pkg_info) };
let status = unsafe { QuerySecurityPackageInfoW(pkg_name.as_ptr(), &mut pkg_info_ptr) };
assert_eq!(status, 0);

let pkg_info = unsafe { pkg_info_ptr.as_mut() }.expect("pkg_info is not null");

// We left all `println`s on purpose:
// to simulate any memory access to the allocated memory.
println!("{:?}", unsafe { &*pkg_info });
println!("{:?}", unsafe { c_w_str_to_string((*pkg_info).name) });
println!("{:?}", unsafe { c_w_str_to_string((*pkg_info).comment) });
println!("{:?}", pkg_info);
println!("{:?}", unsafe { c_w_str_to_string(pkg_info.name) });
println!("{:?}", unsafe { c_w_str_to_string(pkg_info.comment) });

let cb_max_token = unsafe { (*pkg_info).cb_max_token };
let cb_max_token = pkg_info.cb_max_token;

let status = unsafe { FreeContextBuffer(pkg_info.cast()) };
let pv_context_buffer = pkg_info_ptr.cast();
let status = unsafe { FreeContextBuffer(pv_context_buffer) };
assert_eq!(status, 0);

let mut pc_packages = 0;
Expand All @@ -1736,9 +1766,11 @@ mod tests {

for i in 0..pc_packages as usize {
let pkg_info = unsafe { packages.add(i) };
println!("{:?}", unsafe { &*pkg_info });
println!("{:?}", unsafe { c_w_str_to_string((*pkg_info).name) });
println!("{:?}", unsafe { c_w_str_to_string((*pkg_info).comment) });
let pkg_info = unsafe { pkg_info.as_ref() }.expect("pkg_info is not null");

println!("{:?}", pkg_info);
println!("{:?}", unsafe { c_w_str_to_string(pkg_info.name) });
println!("{:?}", unsafe { c_w_str_to_string(pkg_info.comment) });
}

let status = unsafe { FreeContextBuffer(packages.cast()) };
Expand Down Expand Up @@ -1838,20 +1870,23 @@ mod tests {
#[test]
fn initialize_security_context_a() {
let pkg_name = "NTLM\0";
let mut pkg_info: PSecPkgInfoA = null_mut::<SecPkgInfoA>();
let mut pkg_info_ptr: PSecPkgInfoA = null_mut::<SecPkgInfoA>();

let status = unsafe { QuerySecurityPackageInfoA(pkg_name.as_ptr().cast(), &mut pkg_info) };
let status = unsafe { QuerySecurityPackageInfoA(pkg_name.as_ptr().cast(), &mut pkg_info_ptr) };
assert_eq!(status, 0);

let pkg_info = unsafe { pkg_info_ptr.as_mut() }.expect("pkg_info is not null");

// We left all `println`s on purpose:
// to simulate any memory access to the allocated memory.
println!("{:?}", unsafe { &*pkg_info });
println!("{:?}", unsafe { CStr::from_ptr((*pkg_info).name).to_str().unwrap() });
println!("{:?}", unsafe { CStr::from_ptr((*pkg_info).comment).to_str().unwrap() });
println!("{:?}", pkg_info);
println!("{:?}", unsafe { CStr::from_ptr(pkg_info.name) }.to_str().unwrap());
println!("{:?}", unsafe { CStr::from_ptr(pkg_info.comment) }.to_str().unwrap());

let cb_max_token = unsafe { (*pkg_info).cb_max_token };
let cb_max_token = pkg_info.cb_max_token;

let status = unsafe { FreeContextBuffer(pkg_info.cast()) };
let pv_context_buffer = pkg_info_ptr.cast();
let status = unsafe { FreeContextBuffer(pv_context_buffer) };
assert_eq!(status, 0);

let mut pc_packages = 0;
Expand All @@ -1862,9 +1897,11 @@ mod tests {

for i in 0..pc_packages as usize {
let pkg_info = unsafe { packages.add(i) };
println!("{:?}", unsafe { &*pkg_info });
println!("{:?}", unsafe { CStr::from_ptr((*pkg_info).name).to_str().unwrap() });
println!("{:?}", unsafe { CStr::from_ptr((*pkg_info).comment).to_str().unwrap() });
let pkg_info = unsafe { pkg_info.as_ref() }.expect("pkg_info is not null");

println!("{:?}", pkg_info);
println!("{:?}", unsafe { CStr::from_ptr(pkg_info.name) }.to_str().unwrap());
println!("{:?}", unsafe { CStr::from_ptr(pkg_info.comment) }.to_str().unwrap());
}

let status = unsafe { FreeContextBuffer(packages.cast()) };
Expand Down
48 changes: 26 additions & 22 deletions ffi/src/sspi/sec_pkg_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ pub unsafe extern "system" fn QuerySecurityPackageInfoW(
pub type QuerySecurityPackageInfoFnW = unsafe extern "system" fn(*const SecWChar, *mut PSecPkgInfoW) -> SecurityStatus;

#[cfg(test)]
#[expect(
clippy::undocumented_unsafe_blocks,
reason = "undocumented unsafe is acceptable in tests"
)]
mod tests {
use std::ptr::null_mut;

Expand All @@ -490,20 +494,20 @@ mod tests {
let mut packages_amount = 0;
let mut packages = null_mut::<SecPkgInfoA>();

unsafe {
let status = EnumerateSecurityPackagesA(&mut packages_amount, &mut packages);

assert_eq!(status, 0);
assert_eq!(packages_amount, expected_packages_amount);
assert!(!packages.is_null());
let status = unsafe { EnumerateSecurityPackagesA(&mut packages_amount, &mut packages) };

for i in 0..(packages_amount as usize) {
let _ = packages.add(i).as_mut().unwrap();
}
assert_eq!(status, 0);
assert_eq!(packages_amount, expected_packages_amount);
assert!(!packages.is_null());

let status = FreeContextBuffer(packages.cast());
assert_eq!(status, 0);
for i in 0..(packages_amount as usize) {
let pkg_info = unsafe { packages.add(i) };
let _ = unsafe { pkg_info.as_mut() }.expect("pkg_info is not null");
}

let pv_context_buffer = packages.cast();
let status = unsafe { FreeContextBuffer(pv_context_buffer) };
assert_eq!(status, 0);
}

#[test]
Expand All @@ -519,19 +523,19 @@ mod tests {
let mut packages_amount = 0;
let mut packages = null_mut::<SecPkgInfoW>();

unsafe {
let status = EnumerateSecurityPackagesW(&mut packages_amount, &mut packages);

assert_eq!(status, 0);
assert_eq!(packages_amount, expected_packages_amount);
assert!(!packages.is_null());
let status = unsafe { EnumerateSecurityPackagesW(&mut packages_amount, &mut packages) };

for i in 0..(packages_amount as usize) {
let _ = packages.add(i).as_mut().unwrap();
}
assert_eq!(status, 0);
assert_eq!(packages_amount, expected_packages_amount);
assert!(!packages.is_null());

let status = FreeContextBuffer(packages.cast());
assert_eq!(status, 0);
for i in 0..(packages_amount as usize) {
let pkg_info = unsafe { packages.add(i) };
let _ = unsafe { pkg_info.as_mut() }.expect("pkg_info is not null");
}

let pv_context_buffer = packages.cast();
let status = unsafe { FreeContextBuffer(pv_context_buffer) };
assert_eq!(status, 0);
}
}
Loading