Skip to content

Commit d25f80c

Browse files
authored
fix(winscard): fix SCardControl on macOS and pcsc-lite (#749)
### Summary `SCardControl` appears broken in two independent ways in the `system_scard` backend on non-Windows platforms. On macOS the first is a hard crash: the process segfaults inside PCSC.framework when an RDP server sends `CM_IOCTL_GET_FEATURE_REQUEST` for a redirected smart card. Reported from the field on RDM Mac (macOS 26.6.2, SafeNet eToken, NLA + Kerberos smartcard logon); the session logs are analysed below. **1. Calling convention mismatch on macOS (segfault)** PCSC.framework still exports the legacy pcsc-lite 1.1.2 SCardControl: ``` C int32_t SCardControl(SCARDHANDLE hCard, const void *pbSendBuffer, uint32_t cbSendLength, void *pbRecvBuffer, uint32_t *pcbRecvLength); // 5 parameters int32_t SCardControl132(SCARDHANDLE hCard, uint32_t dwControlCode, const void *pbSendBuffer, uint32_t cbSendLength, void *pbRecvBuffer, uint32_t cbRecvLength, uint32_t *lpBytesReturned); // 7 parameters #if !defined(USE_SCARD_CONTROL_112) #define SCardControl SCardControl132 #endif ``` C callers only reach the 7-parameter function through that `#define`. We resolve symbols at runtime with dlsym, so we were getting the 5-parameter function and calling it through a 7-parameter signature. Both symbols exist at distinct addresses, and the 48-byte `SCardControl` thunk is the 1.1.2 shim: ``` cmp w2, #0x10 ; reject if the (shifted) cbSendLength > 16 b.ls +0x10 mov w0, #0x80100004 ; SCARD_E_INVALID_PARAMETER ret mov x2, x1 ; pbSendBuffer ldr w5, [x6] ; cbRecvLength = *pcbRecvLength mov w1, #0 ; dwControlCode = 0 b SCardControl132 ``` Every argument shifted by one slot: the control code landed in `pbSendBuffer`, and the input pointer landed in `cbSendLength`. **Why only GET_FEATURE_REQUEST crashes** This is the non-obvious part, and it explains why the bug stayed latent. The cmp w2, #0x10 guard decides the outcome: |Control request|Shifted `cbSendLength`|Result| |---------------|-------------------------|------| |With a payload|low 32 bits of a real pointer (huge)|fails the guard → `SCARD_E_INVALID_PARAMETER`, graceful| |Zero-length input|Rust's empty-slice pointer, 0x1|passes the guard → dereferences `pbSendBuffer`| In the second case the framework reads from 0x00313520 - the control code reinterpreted as an address. On arm64 macOS __PAGEZERO spans 0x0–0x100000000, so that is guaranteed unmapped: **deterministic SIGSEGV**. `CM_IOCTL_GET_FEATURE_REQUEST` is always zero-length, so it is the one request that reliably kills the process. The no-output `control()` path was unconditionally fatal as well, since it passed `null` for `lpBytesReturned` and the shim does ldr w5, [x6] on it. Verified empirically against the system framework: calling the legacy symbol through the 7-parameter signature returns 0x80100004 with a real send pointer, but 0x80100003 (i.e. it got past parameter validation and reached handle validation) with 0x1. Fixed by loading `SCardControl132` on macOS. This mirrors FreeRDP's own PC/SC backend (winpr/libwinpr/smartcard/smartcard_pcsc.c). The `SCardControl` fallback is deliberately not for older frameworks; falling back to the legacy function would reintroduce the bug. It exists because `PCSC_LITE_LIB_PATH` allows loading a genuine pcsc-lite build on macOS, where `SCardControl` is the 7-parameter function and `SCardControl132` does not exist. **2. Control codes were never translated to the pcsc-lite encoding** | |Encoding|CM_IOCTL_GET_FEATURE_REQUEST| |-|---------|--------------------------------------| |Windows|`CTL_CODE(FILE_DEVICE_SMARTCARD, code, METHOD_BUFFERED, FILE_ANY_ACCESS)`|0x00313520| |pcsc-lite|0x42000000 + code|0x42000D48| The RDP server always sends Windows-encoded codes over rdpdr, and we forwarded them verbatim. Even with the ABI fixed, every control request would have been rejected by the reader driver, so both halves of this patch are needed. Added `win_scard_ctl_code_to_pcsc_lite,` applied in the `SystemScard` control methods on non-Windows targets only; Windows passes codes through unchanged, and codes outside the smart card device type are left untouched so callers already using pcsc-lite encoding keep working. **3. Related soundness fixes** `input.as_ptr()` on an empty slice yields the dangling pointer 0x1. We now pass `null`, which both APIs permit when `cbSendLength` is 0. This is precisely the value that defeats the shim's guard, so it is also defence in depth. `control()` passed `null` for `lpBytesReturned`; pcsc-lite dereferences it unconditionally, so it now receives a valid local. **Evidence from the reported session** The FreeRDP log ends mid-call: these are the final lines of the file, with no `Control_Return`: > smartcard_irp_device_control_decode_request: SCardControl (0x000900D4) FileId: 1 CompletionId: 1 > smartcard_trace_control_call: Control_Call { > smartcard_trace_control_call: dwControlCode: 0x00313520 cbInBufferSize: 0 fpvOutBufferIsNULL: 0 cbOutBufferSize: 256 > smartcard_trace_control_call: pvInBuffer: null > smartcard_trace_control_call: } `dwControlCode`: 0x00313520 is the untranslated Windows encoding, and `cbInBufferSize`: 0 / `pvInBuffer`: `null` is exactly the zero-length shape that reaches the faulting dereference. The sspi-rs log contains no `SCardControl` entry and stops at the immediately preceding `SCardTransmit` return, which is expected: the `fmt` layer uses the default `FmtSpan::NONE`, so `#[instrument(ret)]` only emits on return. No panic-hook output either, consistent with a segfault rather than a Rust panic. **Testing** - Unit tests for the control code translation: `GET_FEATURE_REQUEST`, several `IOCTL_SMARTCARD_*` codes, already-pcsc-lite-encoded values, and non-smart-card device types. - Unit tests pinning the empty-slice pointer behaviour that triggers the crash, and that `control_input_ptr` converts it to `null`. - Verified against the real PCSC.framework on macOS 26 (arm64) that `initialize_pcsc_lite_api` now resolves `SCardControl` to `SCardControl132` (0x238a0d480) instead of the legacy thunk (0x238a0d450). - `cargo clippy -p sspi-ffi --all-targets clean; cargo test -p sspi-ffi --lib` passes (32 tests). - Compile-checked the `Dword` = `c_ulong` configuration used on Linux, since `Dword` is u32 only on macOS.
1 parent f73e31c commit d25f80c

2 files changed

Lines changed: 169 additions & 19 deletions

File tree

‎ffi/src/winscard/pcsc_lite/mod.rs‎

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,31 @@ impl From<State> for winscard::winscard::State {
121121
/// The user can use this environment variable to customize the `pcsc-lite` library loading.
122122
const PCSC_LITE_LIB_PATH_ENV: &str = "PCSC_LITE_LIB_PATH";
123123

124+
/// Windows device type for the smart card IOCTLs.
125+
///
126+
/// `#define FILE_DEVICE_SMARTCARD 0x00000031`
127+
const FILE_DEVICE_SMARTCARD: u32 = 0x0000_0031;
128+
129+
/// Translates a Windows smart card control code into its `pcsc-lite` counterpart.
130+
///
131+
/// Windows encodes `SCARD_CTL_CODE(code)` as `0x00310000 | (code << 2)`, `pcsc-lite` as
132+
/// `0x42000000 + code`. RDP servers always send the Windows encoding.
133+
///
134+
/// Control codes that do not belong to the smart card device type are returned unchanged.
135+
pub fn win_scard_ctl_code_to_pcsc_lite(control_code: u32) -> u32 {
136+
// `#define DEVICE_TYPE_FROM_CTL_CODE(ctrlCode) (((DWORD)(ctrlCode & 0xFFFF0000)) >> 16)`
137+
if (control_code >> 16) != FILE_DEVICE_SMARTCARD {
138+
return control_code;
139+
}
140+
141+
// `#define FUNCTION_FROM_CTL_CODE(ctrlCode) ((DWORD)((ctrlCode >> 2) & 0xFFF))`
142+
0x4200_0000 + ((control_code >> 2) & 0xFFF)
143+
}
144+
124145
pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
125-
let pcsc_lite_path = if let Ok(lib_path) = env::var(PCSC_LITE_LIB_PATH_ENV) {
126-
Cow::Owned(lib_path)
146+
let user_selected_lib = env::var(PCSC_LITE_LIB_PATH_ENV).ok();
147+
let pcsc_lite_path = if let Some(lib_path) = user_selected_lib.as_deref() {
148+
Cow::Borrowed(lib_path)
127149
} else {
128150
#[cfg(target_os = "macos")]
129151
{
@@ -147,16 +169,22 @@ pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
147169
));
148170
}
149171

150-
macro_rules! load_fn {
151-
($func_name:literal) => {{
152-
let fn_name = CString::new($func_name).expect("CString creation should not fail");
172+
macro_rules! load_sym {
173+
($sym_name:literal) => {{
174+
let sym_name = CString::new($sym_name).expect("CString creation should not fail");
153175

154176
// SAFETY:
155177
// - We've checked the `handle` above.
156-
// - `fn_name` is correct and hardcoded in the code.
157-
let fn_ptr = unsafe { dlsym(handle, fn_name.as_ptr()) };
158-
debug!(?fn_ptr, $func_name);
178+
// - `sym_name` is correct and hardcoded in the code.
179+
let sym_ptr = unsafe { dlsym(handle, sym_name.as_ptr()) };
180+
debug!(?sym_ptr, $sym_name);
159181

182+
sym_ptr
183+
}};
184+
}
185+
186+
macro_rules! transmute_fn {
187+
($fn_ptr:expr) => {{
160188
// SAFETY:
161189
// - `*mut c_void` and target transmute type are both C pointers. They have the same layout.
162190
// Thus, we can safely transmute the C pointer to the C function pointer.
@@ -165,11 +193,18 @@ pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
165193
unsafe {
166194
// Not great to silent, but mostly fine in this context.
167195
#[expect(clippy::missing_transmute_annotations)]
168-
std::mem::transmute::<*mut libc::c_void, _>(fn_ptr)
196+
std::mem::transmute::<*mut libc::c_void, _>($fn_ptr)
169197
}
170198
}};
171199
}
172200

201+
macro_rules! load_fn {
202+
($func_name:literal) => {{
203+
let fn_ptr = load_sym!($func_name);
204+
transmute_fn!(fn_ptr)
205+
}};
206+
}
207+
173208
macro_rules! load_io_request {
174209
($req_name:literal) => {{
175210
let req_name = CString::new($req_name).expect("CString creation should not fail");
@@ -189,6 +224,34 @@ pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
189224
}};
190225
}
191226

227+
// `PCSC.framework` exports both the legacy pcsc-lite 1.1.2 `SCardControl` (5 parameters) and
228+
// `SCardControl132` (7 parameters); its header redirects the former to the latter. A real pcsc-lite
229+
// build exports only `SCardControl`, with 7 parameters. Since both libraries can be loaded here, and
230+
// the two `SCardControl` variants are indistinguishable by symbol name, only accept the legacy name
231+
// from a library the user pointed us at explicitly.
232+
#[cfg(target_os = "macos")]
233+
let scard_control = {
234+
let mut fn_ptr = load_sym!("SCardControl132");
235+
236+
if fn_ptr.is_null() && user_selected_lib.is_some() {
237+
fn_ptr = load_sym!("SCardControl");
238+
}
239+
240+
if fn_ptr.is_null() {
241+
return Err(Error::new(
242+
ErrorKind::InternalError,
243+
format!(
244+
"pcsc-lite library does not export SCardControl132: {}",
245+
pcsc_lite_path.to_str().unwrap()
246+
),
247+
));
248+
}
249+
250+
transmute_fn!(fn_ptr)
251+
};
252+
#[cfg(not(target_os = "macos"))]
253+
let scard_control = load_fn!("SCardControl");
254+
192255
Ok(PcscLiteApiFunctionTable {
193256
SCardEstablishContext: load_fn!("SCardEstablishContext"),
194257
SCardReleaseContext: load_fn!("SCardReleaseContext"),
@@ -199,7 +262,7 @@ pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
199262
SCardEndTransaction: load_fn!("SCardEndTransaction"),
200263
SCardStatus: load_fn!("SCardStatus"),
201264
SCardGetStatusChange: load_fn!("SCardGetStatusChange"),
202-
SCardControl: load_fn!("SCardControl"),
265+
SCardControl: scard_control,
203266
SCardGetAttrib: load_fn!("SCardGetAttrib"),
204267
SCardSetAttrib: load_fn!("SCardSetAttrib"),
205268
SCardTransmit: load_fn!("SCardTransmit"),
@@ -214,3 +277,36 @@ pub fn initialize_pcsc_lite_api() -> WinScardResult<PcscLiteApiFunctionTable> {
214277
g_rgSCardRawPci: load_io_request!("g_rgSCardRawPci"),
215278
})
216279
}
280+
281+
#[cfg(test)]
282+
mod tests {
283+
use super::win_scard_ctl_code_to_pcsc_lite;
284+
285+
#[test]
286+
fn get_feature_request_control_code_is_translated() {
287+
// `#define CM_IOCTL_GET_FEATURE_REQUEST SCARD_CTL_CODE(3400)`
288+
assert_eq!(0x4200_0D48, win_scard_ctl_code_to_pcsc_lite(0x0031_3520));
289+
}
290+
291+
#[test]
292+
fn smart_card_control_codes_are_translated() {
293+
// `#define IOCTL_SMARTCARD_POWER SCARD_CTL_CODE(1)`
294+
assert_eq!(0x4200_0001, win_scard_ctl_code_to_pcsc_lite(0x0031_0004));
295+
// `#define IOCTL_SMARTCARD_GET_ATTRIBUTE SCARD_CTL_CODE(2)`
296+
assert_eq!(0x4200_0002, win_scard_ctl_code_to_pcsc_lite(0x0031_0008));
297+
// `#define IOCTL_SMARTCARD_GET_STATE SCARD_CTL_CODE(14)`
298+
assert_eq!(0x4200_000E, win_scard_ctl_code_to_pcsc_lite(0x0031_0038));
299+
}
300+
301+
#[test]
302+
fn pcsc_lite_control_codes_are_passed_through() {
303+
assert_eq!(0x4200_0D48, win_scard_ctl_code_to_pcsc_lite(0x4200_0D48));
304+
assert_eq!(0x4200_0001, win_scard_ctl_code_to_pcsc_lite(0x4200_0001));
305+
}
306+
307+
#[test]
308+
fn non_smart_card_control_codes_are_passed_through() {
309+
// `FILE_DEVICE_KEYBOARD`.
310+
assert_eq!(0x000B_0004, win_scard_ctl_code_to_pcsc_lite(0x000B_0004));
311+
}
312+
}

‎ffi/src/winscard/system_scard/card.rs‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22
use std::fmt;
3-
use std::ptr::null_mut;
3+
use std::ptr::{null, null_mut};
44

55
#[cfg(target_os = "windows")]
66
use ffi_types::winscard::functions::SCardApiFunctionTable;
@@ -27,6 +27,34 @@ enum HandleState {
2727
Connected(ScardHandle),
2828
}
2929

30+
/// Converts the [ControlCode] into the value expected by the underlying smart card API.
31+
#[cfg(target_os = "windows")]
32+
fn native_control_code(code: ControlCode) -> u32 {
33+
code
34+
}
35+
36+
/// Converts the [ControlCode] into the value expected by the underlying smart card API.
37+
///
38+
/// See [`crate::winscard::pcsc_lite::win_scard_ctl_code_to_pcsc_lite`].
39+
#[cfg(not(target_os = "windows"))]
40+
fn native_control_code(code: ControlCode) -> crate::winscard::pcsc_lite::Dword {
41+
// `Dword` is `u32` on macOS and `c_ulong` elsewhere.
42+
#[allow(clippy::useless_conversion)]
43+
crate::winscard::pcsc_lite::win_scard_ctl_code_to_pcsc_lite(code).into()
44+
}
45+
46+
/// Returns a pointer to the `SCardControl` input buffer.
47+
///
48+
/// An empty slice has a dangling pointer, so a null pointer is returned instead. Both `WinSCard`
49+
/// and `pcsc-lite` allow `pbSendBuffer` to be null when `cbSendLength` is 0.
50+
fn control_input_ptr(input: &[u8]) -> *const core::ffi::c_void {
51+
if input.is_empty() {
52+
null()
53+
} else {
54+
input.as_ptr().cast()
55+
}
56+
}
57+
3058
/// Represents a system-provided smart card.
3159
///
3260
/// _Hint:_ It's **always better** to explicitly disconnect the card using the [SystemScard::disconnect] method.
@@ -217,22 +245,27 @@ impl WinScard for SystemScard {
217245
// What if the `code` will be corresponding to operation with output and
218246
// user will use this method instead of `control_with_output`? Then safety conditions will be violated.
219247
fn control(&mut self, code: ControlCode, input: &[u8]) -> WinScardResult<()> {
248+
let control_code = native_control_code(code);
249+
let input_ptr = control_input_ptr(input);
250+
// pcsc-lite dereferences `lpBytesReturned` unconditionally.
251+
let mut bytes_returned = 0;
252+
220253
try_execute!(
221254
// SAFETY:
222255
// - `h_card` is set by a previous call to `SCardConnectA`.
223-
// - `input.as_ptr()` is a valid, readable pointer to a
256+
// - `input_ptr` is null or a valid, readable pointer to a slice of `input.len()` bytes.
224257
// - `lpOutBuffer` can be null.
225258
// - `cbOutBufferSize` is 0 because `lpOutBuffer` is null.
226-
// - `lpBytesReturned` can be null if `lpOutBuffer` is null.
259+
// - `&mut bytes_returned` is a properly-aligned, writable pointer to a local variable.
227260
unsafe {
228261
(self.api.SCardControl)(
229262
self.h_card()?,
230-
code.into(),
231-
input.as_ptr().cast(),
263+
control_code,
264+
input_ptr,
232265
input.len().try_into()?,
233266
null_mut(),
234267
0,
235-
null_mut(),
268+
&mut bytes_returned,
236269
)
237270
},
238271
"SCardControl failed"
@@ -242,21 +275,23 @@ impl WinScard for SystemScard {
242275
}
243276

244277
fn control_with_output(&mut self, code: ControlCode, input: &[u8], output: &mut [u8]) -> WinScardResult<usize> {
278+
let control_code = native_control_code(code);
279+
let input_ptr = control_input_ptr(input);
245280
let mut receive_len = 0;
246281
let output_buf_len = output.len().try_into()?;
247282

248283
try_execute!(
249284
// SAFETY:
250285
// - `h_card` is set by a previous call to `SCardConnectA`.
251-
// - `input.as_ptr()` is a valid, readable pointer to a slice.
286+
// - `input_ptr` is null or a valid, readable pointer to a slice of `input.len()` bytes.
252287
// - `output` is a properly-aligned, writable pointer to a valid slice with size of `output_buf_len`.
253288
// - `output_buf_len` is valid length for `output` buffer.
254289
// - `&mut receive_len` is a properly-aligned, writable pointer to a local variable.
255290
unsafe {
256291
(self.api.SCardControl)(
257292
self.h_card()?,
258-
code.into(),
259-
input.as_ptr().cast(),
293+
control_code,
294+
input_ptr,
260295
input.len().try_into()?,
261296
output.as_mut_ptr().cast(),
262297
output_buf_len,
@@ -443,3 +478,22 @@ impl WinScard for SystemScard {
443478
Ok(())
444479
}
445480
}
481+
482+
#[cfg(test)]
483+
mod tests {
484+
use std::ptr::null;
485+
486+
use super::control_input_ptr;
487+
488+
#[test]
489+
fn empty_control_input_is_a_null_pointer() {
490+
let empty: &[u8] = &[];
491+
assert_eq!(null(), control_input_ptr(empty));
492+
}
493+
494+
#[test]
495+
fn non_empty_control_input_points_to_the_data() {
496+
let input = [1_u8, 2, 3];
497+
assert_eq!(input.as_ptr().cast(), control_input_ptr(&input));
498+
}
499+
}

0 commit comments

Comments
 (0)