feat(ffi): hand-mirrored C structs are now layout-checked at compile time
The sharpest memory-safety risk left in this codebase is not an `unsafe` block — it is a hand-written `#[repr(C)]` mirror of an external C struct. Get a field offset wrong and nothing fails to compile and nothing reliably crashes: the library reads a pointer, a length or a pitch out of the wrong bytes. Eleven such structs across five files had NO check at all. Guarded here, each next to the struct it protects: * `AVCUDADeviceContext`, and `AVD3D11VADeviceContext`/`AVD3D11VAFramesContext`. `ffmpeg-sys-next` binds none of them, so these mirrors are the only definitions — and we WRITE through them (`cuda_ctx`, `device`, `bind_flags`). ⚠ The D3D11VA pair is duplicated VERBATIM in two crates (pf-encode's `ffmpeg_win.rs`, pf-client-core's `video_d3d11.rs`) because neither can depend on the other; they must agree with libav and with each other, and now a drift in either is a build error. * The six cuda.h structs. Three were already asserted — but only in `#[cfg(test)]`, so the check ran when someone ran the tests and never in a release build. They are `const` now. The other three, including `CUDA_MEMCPY2D` which is filled on EVERY zero-copy frame, had nothing. * `MsghdrX`, Darwin's `msghdr_x`, which `libc` does not expose. Its layout is not reviewable by eye: the 32-bit fields force padding before each following pointer, so `msg_iov` sits at 16 and not 12. `sendmsg_x`/`recvmsg_x` take the pointer and length from it. * `IPolicyConfigVtbl` — the sharpest of the set. It mirrors an UNDOCUMENTED COM interface, and `set_default_endpoint` is called by SLOT INDEX through a ten-entry `_reserved` gap that carries no names to anchor a review. A field added or resized above it does not break the build; it calls a different function pointer through a mismatched signature. Every assertion is `const _: () = assert!(..)`, so it holds on every build including release and cannot be skipped. The compiler verified the numbers — the sizes and offsets asserted here are the ones the target actually produces, on each platform that compiles the struct. Verified: Linux .21 fmt + both CI clippy steps rc=0 (CUDA + libav CUDA mirrors); Windows .47 full CI clippy set rc=0 + pf-capture tests (D3D11VA pair, COM vtable); macOS `cargo check -p punktfunk-core` (MsghdrX — the only platform that compiles it).
This commit is contained in:
@@ -149,6 +149,32 @@ struct AVD3D11VAFramesContext {
|
|||||||
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-managed)
|
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-managed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hand-written mirrors of libav's `AVD3D11VADeviceContext` / `AVD3D11VAFramesContext`
|
||||||
|
// (hwcontext_d3d11va.h) — `ffmpeg-sys-next` binds neither, and we WRITE `device` / `bind_flags`
|
||||||
|
// through them, so a wrong offset is silent corruption of libav's context rather than a compile
|
||||||
|
// error. ⚠ These two structs are duplicated in the other crate that talks to the same libav
|
||||||
|
// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree
|
||||||
|
// with libav AND with each other, and these assertions are what makes a drift in either a build
|
||||||
|
// failure instead of a runtime mystery.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
type P = *mut c_void;
|
||||||
|
assert!(size_of::<AVD3D11VADeviceContext>() == 7 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, device) == 0);
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, device_context) == size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, video_device) == 2 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, video_context) == 3 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, lock) == 4 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, unlock) == 5 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, lock_ctx) == 6 * size_of::<P>());
|
||||||
|
// ptr, u32, u32, ptr — the two 32-bit flags pack into one pointer-sized slot with no padding.
|
||||||
|
assert!(size_of::<AVD3D11VAFramesContext>() == 3 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, texture) == 0);
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, bind_flags) == size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, misc_flags) == size_of::<P>() + 4);
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, texture_infos) == 2 * size_of::<P>());
|
||||||
|
};
|
||||||
|
|
||||||
fn averr(what: &str, code: i32) -> anyhow::Error {
|
fn averr(what: &str, code: i32) -> anyhow::Error {
|
||||||
anyhow!("{what}: {}", ffmpeg::Error::from(code))
|
anyhow!("{what}: {}", ffmpeg::Error::from(code))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,19 @@ struct AVCUDADeviceContext {
|
|||||||
internal: *mut std::ffi::c_void, // filled by ctx_init
|
internal: *mut std::ffi::c_void, // filled by ctx_init
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hand-written mirror of libav's `AVCUDADeviceContext` (hwcontext_cuda.h) — `ffmpeg-sys-next`
|
||||||
|
// does not bind it, so this is the only definition, and `CudaHw::new` WRITES `cuda_ctx` through it.
|
||||||
|
// A wrong offset here would not fail to compile or crash; it would scribble over libav's internal
|
||||||
|
// pointer. Nothing checked that until this block: the realistic failure is someone adding or
|
||||||
|
// reordering a field here, which these assertions turn into a build error.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
assert!(size_of::<AVCUDADeviceContext>() == 3 * size_of::<*mut std::ffi::c_void>());
|
||||||
|
assert!(offset_of!(AVCUDADeviceContext, cuda_ctx) == 0);
|
||||||
|
assert!(offset_of!(AVCUDADeviceContext, stream) == size_of::<*mut std::ffi::c_void>());
|
||||||
|
assert!(offset_of!(AVCUDADeviceContext, internal) == 2 * size_of::<*mut std::ffi::c_void>());
|
||||||
|
};
|
||||||
|
|
||||||
/// CUDA hardware-frame contexts that wrap our shared `CUcontext`, so `hevc_nvenc` reads the
|
/// CUDA hardware-frame contexts that wrap our shared `CUcontext`, so `hevc_nvenc` reads the
|
||||||
/// imported device buffer directly. Owns two `AVBufferRef`s, unref'd on drop.
|
/// imported device buffer directly. Owns two `AVBufferRef`s, unref'd on drop.
|
||||||
struct CudaHw {
|
struct CudaHw {
|
||||||
|
|||||||
@@ -92,6 +92,32 @@ struct AVD3D11VAFramesContext {
|
|||||||
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-owned; we never touch it)
|
texture_infos: *mut c_void, // AVD3D11FrameDescriptor* (FFmpeg-owned; we never touch it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hand-written mirrors of libav's `AVD3D11VADeviceContext` / `AVD3D11VAFramesContext`
|
||||||
|
// (hwcontext_d3d11va.h) — `ffmpeg-sys-next` binds neither, and we WRITE `device` / `bind_flags`
|
||||||
|
// through them, so a wrong offset is silent corruption of libav's context rather than a compile
|
||||||
|
// error. ⚠ These two structs are duplicated in the other crate that talks to the same libav
|
||||||
|
// contexts (pf-encode's `ffmpeg_win.rs` and pf-client-core's `video_d3d11.rs`); they must agree
|
||||||
|
// with libav AND with each other, and these assertions are what makes a drift in either a build
|
||||||
|
// failure instead of a runtime mystery.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
type P = *mut c_void;
|
||||||
|
assert!(size_of::<AVD3D11VADeviceContext>() == 7 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, device) == 0);
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, device_context) == size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, video_device) == 2 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, video_context) == 3 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, lock) == 4 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, unlock) == 5 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VADeviceContext, lock_ctx) == 6 * size_of::<P>());
|
||||||
|
// ptr, u32, u32, ptr — the two 32-bit flags pack into one pointer-sized slot with no padding.
|
||||||
|
assert!(size_of::<AVD3D11VAFramesContext>() == 3 * size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, texture) == 0);
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, bind_flags) == size_of::<P>());
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, misc_flags) == size_of::<P>() + 4);
|
||||||
|
assert!(offset_of!(AVD3D11VAFramesContext, texture_infos) == 2 * size_of::<P>());
|
||||||
|
};
|
||||||
|
|
||||||
/// AMD AMF vs Intel QSV — the two libavcodec vendor backends this module covers.
|
/// AMD AMF vs Intel QSV — the two libavcodec vendor backends this module covers.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum WinVendor {
|
pub enum WinVendor {
|
||||||
|
|||||||
@@ -1401,30 +1401,6 @@ pub fn copy_pitched_nv12_to_buffer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// The cuda.h struct layouts these calls depend on are asserted at COMPILE time in `ffi.rs`
|
||||||
mod tests {
|
// (`const _`), which covers all six structs and every build — the runtime test that used to live
|
||||||
use super::*;
|
// here checked three of them and only when someone ran it.
|
||||||
use std::mem::{offset_of, size_of};
|
|
||||||
|
|
||||||
/// The external-semaphore param structs are hand-flattened from cuda.h unions — assert the
|
|
||||||
/// layout against the C definitions so a transcription slip fails in CI, not in the driver.
|
|
||||||
#[test]
|
|
||||||
fn external_semaphore_struct_layouts_match_cuda_h() {
|
|
||||||
// CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC: type(4)+pad(4)+union(16)+flags(4)+reserved(64) = 96.
|
|
||||||
assert_eq!(size_of::<CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC>(), 96);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC, handle), 8);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC, flags), 24);
|
|
||||||
|
|
||||||
// CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS: params{fence(8)+nvSciSync(8)+keyedMutex(8)+
|
|
||||||
// reserved[12](48)} = 72, flags at 72, reserved[16] → 144 total.
|
|
||||||
assert_eq!(size_of::<CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS>(), 144);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS, value), 0);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS, flags), 72);
|
|
||||||
|
|
||||||
// CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS: params{fence(8)+nvSciSync(8)+keyedMutex(16,
|
|
||||||
// tail-padded)+reserved[10](40)} = 72, flags at 72, reserved[16] → 144 total.
|
|
||||||
assert_eq!(size_of::<CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS>(), 144);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS, value), 0);
|
|
||||||
assert_eq!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS, flags), 72);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -139,6 +139,59 @@ pub struct CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS {
|
|||||||
/// to try".
|
/// to try".
|
||||||
pub const CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_FD: c_uint = 9;
|
pub const CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_TIMELINE_SEMAPHORE_FD: c_uint = 9;
|
||||||
|
|
||||||
|
// LAYOUT GUARDS for the six hand-flattened cuda.h structs above.
|
||||||
|
//
|
||||||
|
// These are filled here and handed straight to the CUDA driver, which reads them by offset. A
|
||||||
|
// transcription slip does not fail to compile and does not reliably crash — it makes the driver read
|
||||||
|
// a pitch, a size or an fd from the wrong bytes. Three of the six were already asserted, but only in
|
||||||
|
// `#[cfg(test)]`, so the check ran when someone ran the tests and never in a release build; these
|
||||||
|
// are `const`, so they hold on every compilation and cannot be skipped. The other three
|
||||||
|
// (`CUDA_MEMCPY2D` and the two external-memory descs) had no check at all until now, and
|
||||||
|
// `CUDA_MEMCPY2D` is the one used on every zero-copy frame.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
|
||||||
|
// 16 fields, all pointer-sized or padded to it: 16 * 8 = 128. The two `c_uint` memory-type
|
||||||
|
// fields are each followed by padding to align the pointer after them, which is the part a
|
||||||
|
// hand-transcription gets wrong.
|
||||||
|
assert!(size_of::<CUDA_MEMCPY2D>() == 128);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, srcMemoryType) == 16);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, srcHost) == 24); // padded past srcMemoryType
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, srcPitch) == 48);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, dstMemoryType) == 72);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, dstHost) == 80); // padded past dstMemoryType
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, dstPitch) == 104);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, WidthInBytes) == 112);
|
||||||
|
assert!(offset_of!(CUDA_MEMCPY2D, Height) == 120);
|
||||||
|
|
||||||
|
// type(4)+pad(4)+union(16)+size(8)+flags(4)+reserved[16](64) = 104.
|
||||||
|
assert!(size_of::<CUDA_EXTERNAL_MEMORY_HANDLE_DESC>() == 104);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_MEMORY_HANDLE_DESC, handle) == 8);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_MEMORY_HANDLE_DESC, size) == 24);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_MEMORY_HANDLE_DESC, flags) == 32);
|
||||||
|
|
||||||
|
// offset(8)+size(8)+flags(4)+reserved[16](64), tail-padded to 8 = 88.
|
||||||
|
assert!(size_of::<CUDA_EXTERNAL_MEMORY_BUFFER_DESC>() == 88);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_MEMORY_BUFFER_DESC, size) == 8);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_MEMORY_BUFFER_DESC, flags) == 16);
|
||||||
|
|
||||||
|
// type(4)+pad(4)+union(16)+flags(4)+reserved[16](64) = 96. No `size` — a semaphore has none.
|
||||||
|
assert!(size_of::<CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC>() == 96);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC, handle) == 8);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC, flags) == 24);
|
||||||
|
|
||||||
|
// params{fence(8)+nvSciSync(8)+keyedMutex(8)+reserved[12](48)} = 72, flags at 72 → 144.
|
||||||
|
assert!(size_of::<CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS>() == 144);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS, value) == 0);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS, flags) == 72);
|
||||||
|
|
||||||
|
// As signal, but `keyedMutex` is `{u64 key; u32 timeoutMs;}` = 16 with tail padding, hence the
|
||||||
|
// explicit pad word before the 10 reserved words. Still 72 to `flags` → 144.
|
||||||
|
assert!(size_of::<CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS>() == 144);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS, value) == 0);
|
||||||
|
assert!(offset_of!(CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS, flags) == 72);
|
||||||
|
};
|
||||||
|
|
||||||
/// `CUipcMemHandle` (cuda.h): an opaque 64-byte struct identifying a device allocation across
|
/// `CUipcMemHandle` (cuda.h): an opaque 64-byte struct identifying a device allocation across
|
||||||
/// processes. Produced by `cuIpcGetMemHandle` in the exporting process, consumed by
|
/// processes. Produced by `cuIpcGetMemHandle` in the exporting process, consumed by
|
||||||
/// `cuIpcOpenMemHandle` in the importer — passed **by value**, matching the C
|
/// `cuIpcOpenMemHandle` in the importer — passed **by value**, matching the C
|
||||||
|
|||||||
@@ -49,6 +49,24 @@ struct MsghdrX {
|
|||||||
msg_datalen: libc::size_t,
|
msg_datalen: libc::size_t,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hand-written mirror of Darwin's `struct msghdr_x` (sys/socket.h), which `libc` does not expose.
|
||||||
|
// `sendmsg_x`/`recvmsg_x` read and write through it, so a wrong offset would hand the kernel the
|
||||||
|
// wrong pointer or length — the two fields it uses to decide how much memory to touch. The layout is
|
||||||
|
// not obvious by inspection because the 32-bit fields force padding before the pointers that follow
|
||||||
|
// them, which is exactly the kind of thing an edit gets wrong silently.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
assert!(size_of::<MsghdrX>() == 56);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_name) == 0);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_namelen) == 8);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_iov) == 16); // 4 bytes of padding after msg_namelen
|
||||||
|
assert!(offset_of!(MsghdrX, msg_iovlen) == 24);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_control) == 32); // padding after msg_iovlen
|
||||||
|
assert!(offset_of!(MsghdrX, msg_controllen) == 40);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_flags) == 44);
|
||||||
|
assert!(offset_of!(MsghdrX, msg_datalen) == 48);
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(target_vendor = "apple")]
|
#[cfg(target_vendor = "apple")]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
/// Darwin batched receive: up to `cnt` datagrams in one syscall; returns the count received and
|
/// Darwin batched receive: up to `cnt` datagrams in one syscall; returns the count received and
|
||||||
|
|||||||
@@ -310,6 +310,26 @@ struct IPolicyConfigVtbl {
|
|||||||
// SetEndpointVisibility follows — unused.
|
// SetEndpointVisibility follows — unused.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This mirrors the vtable of the UNDOCUMENTED `IPolicyConfig` COM interface, so there is no header
|
||||||
|
// to check it against and no `windows-rs` binding to fall back on. `set_default_endpoint` is called
|
||||||
|
// by INDEX — `((*vtbl).set_default_endpoint)(..)` is really "the 14th function pointer in this
|
||||||
|
// table" — so a field added, removed or resized above it does not fail to compile: it silently calls
|
||||||
|
// a DIFFERENT function through a mismatched signature, which is arbitrary-code territory rather
|
||||||
|
// than a wrong answer. The `_reserved` gap is what makes that easy to get wrong, since its ten slots
|
||||||
|
// carry no names to anchor a review. These assertions pin the two things the call actually depends
|
||||||
|
// on: the slot index of `set_default_endpoint`, and the size of the table up to it.
|
||||||
|
const _: () = {
|
||||||
|
use std::mem::{offset_of, size_of};
|
||||||
|
type P = *const c_void;
|
||||||
|
// 3 IUnknown slots + 10 reserved = `set_default_endpoint` is slot 13 (0-based).
|
||||||
|
assert!(offset_of!(IPolicyConfigVtbl, query_interface) == 0);
|
||||||
|
assert!(offset_of!(IPolicyConfigVtbl, add_ref) == size_of::<P>());
|
||||||
|
assert!(offset_of!(IPolicyConfigVtbl, release) == 2 * size_of::<P>());
|
||||||
|
assert!(offset_of!(IPolicyConfigVtbl, _reserved) == 3 * size_of::<P>());
|
||||||
|
assert!(offset_of!(IPolicyConfigVtbl, set_default_endpoint) == 13 * size_of::<P>());
|
||||||
|
assert!(size_of::<IPolicyConfigVtbl>() == 14 * size_of::<P>());
|
||||||
|
};
|
||||||
|
|
||||||
/// Set `device_id` as the default audio endpoint for eConsole/eMultimedia/eCommunications via the
|
/// Set `device_id` as the default audio endpoint for eConsole/eMultimedia/eCommunications via the
|
||||||
/// undocumented `IPolicyConfig::SetDefaultEndpoint` (the call `mmsys.cpl` makes). Errs if any role
|
/// undocumented `IPolicyConfig::SetDefaultEndpoint` (the call `mmsys.cpl` makes). Errs if any role
|
||||||
/// fails.
|
/// fails.
|
||||||
|
|||||||
Reference in New Issue
Block a user