From 1048322c17d3ebbad22dccc668e11ed4a026fe88 Mon Sep 17 00:00:00 2001 From: Sunli Date: Wed, 26 Aug 2026 10:08:14 +0800 Subject: [PATCH] fix(c): tolerate null pointers for empty list arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `std::vector::data()` is allowed to return `nullptr` for an empty vector, and that is exactly what the C++ binding passes when a list argument is omitted. The C layer fed the pointer straight to `std::slice::from_raw_parts`, which requires a non-null, aligned pointer even for a zero-length slice — so the call was undefined behaviour and **aborted the process** under the debug UB checks: ``` thread '' panicked at c/src/quote_context/context.rs: unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be aligned and non-null, and the total size of the slice not to exceed `isize::MAX` thread caused non-unwinding panic. aborting. ``` Reproduced live by calling `QuoteContext::warrant_list` from C++ with no filters — the most natural way to call it. All 17 `from_raw_parts` call sites had this shape, so they now go through a single null-tolerant helper in `c/src/types/mod.rs`: ```rust pub(crate) unsafe fn slice_from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] { if len == 0 || data.is_null() { &[] } else { std::slice::from_raw_parts(data, len) } } ``` Covers `quote_context` (8), `trade_context` (5), `agent_context` (2), `alert_context` (1) and `cstr_array_to_rust` in `types` (1). Internal only — no change to the generated `longbridge.h`, so the C and C++ public APIs are untouched. --- CHANGELOG.md | 4 ++++ c/src/agent_context/context.rs | 6 +++--- c/src/alert_context/types.rs | 4 ++-- c/src/quote_context/context.rs | 21 ++++++++++++--------- c/src/trade_context/context.rs | 12 ++++++------ c/src/types/mod.rs | 18 +++++++++++++++++- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1885701d7..ba39e6bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **C/C++ SDKs:** every list argument that crosses the FFI boundary now tolerates a null pointer with a zero length. `std::vector::data()` is allowed to return `nullptr` for an empty vector, which is exactly what the C++ binding passes for an omitted list argument, but the C layer fed it straight to `std::slice::from_raw_parts` — undefined behaviour that **aborts the process** under the debug UB checks. Hit live by `QuoteContext::warrant_list` with no filters (`c/src/quote_context/context.rs`); all 17 call sites across `quote_context`, `trade_context`, `agent_context`, `alert_context`, and `types` now go through a null-tolerant `slice_from_raw_parts` helper + ### Added - **Rust:** `Signal.status` is now a `SignalStatus` enum (pending / active / deleted / ai-failed / filtered-by-manual / ai-submit-failed), `SignalsResponse.total` is `i32` to match the wire contract, and the `risk_level` / `display_control` fields were dropped — neither is part of the API contract nor served in production diff --git a/c/src/agent_context/context.rs b/c/src/agent_context/context.rs index 140c99e11..cf4abc9f6 100644 --- a/c/src/agent_context/context.rs +++ b/c/src/agent_context/context.rs @@ -13,7 +13,7 @@ use crate::{ async_call::{CAsyncCallback, execute_async}, callback::CFreeUserDataFunc, config::CConfig, - types::{CCow, ToFFI, cstr_to_rust}, + types::{CCow, ToFFI, cstr_to_rust, slice_from_raw_parts}, }; /// AI Agent conversation context @@ -65,10 +65,10 @@ unsafe fn answers_from_ffi( num_answers: usize, ) -> AnswersByToolCall { let mut map: AnswersByToolCall = HashMap::new(); - for entry in std::slice::from_raw_parts(answers, num_answers) { + for entry in slice_from_raw_parts(answers, num_answers) { let tool_call_id = cstr_to_rust(entry.tool_call_id); let mut questions = HashMap::new(); - for qa in std::slice::from_raw_parts(entry.answers, entry.num_answers) { + for qa in slice_from_raw_parts(entry.answers, entry.num_answers) { questions.insert(cstr_to_rust(qa.question), cstr_to_rust(qa.answer)); } map.insert(tool_call_id, questions); diff --git a/c/src/alert_context/types.rs b/c/src/alert_context/types.rs index 338c1e79b..099714486 100644 --- a/c/src/alert_context/types.rs +++ b/c/src/alert_context/types.rs @@ -2,7 +2,7 @@ use std::os::raw::c_char; use longbridge::alert::{AlertItem, AlertList, AlertSymbolGroup}; -use crate::types::{CString, CVec, ToFFI}; +use crate::types::{CString, CVec, ToFFI, slice_from_raw_parts}; /// A single alert indicator configuration for a symbol. #[repr(C)] @@ -63,7 +63,7 @@ impl CAlertItem { /// `state` pointer must point to at least `num_state` valid `i32` values. pub unsafe fn to_alert_item(&self) -> longbridge::alert::AlertItem { use crate::types::cstr_to_rust; - let state = std::slice::from_raw_parts(self.state, self.num_state).to_vec(); + let state = slice_from_raw_parts(self.state, self.num_state).to_vec(); let value_map_str = cstr_to_rust(self.value_map); let value_map = serde_json::from_str(&value_map_str).unwrap_or(serde_json::Value::Null); longbridge::alert::AlertItem { diff --git a/c/src/quote_context/context.rs b/c/src/quote_context/context.rs index ce9feef9b..a0a1babde 100644 --- a/c/src/quote_context/context.rs +++ b/c/src/quote_context/context.rs @@ -33,7 +33,10 @@ use crate::{ LB_WATCHLIST_GROUP_NAME, LB_WATCHLIST_GROUP_SECURITIES, }, }, - types::{CCow, CDate, CDateTime, CMarket, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust}, + types::{ + CCow, CDate, CDateTime, CMarket, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust, + slice_from_raw_parts, + }, }; pub type COnQuoteCallback = extern "C" fn(*const CQuoteContext, *const CPushQuote, *mut c_void); @@ -781,23 +784,23 @@ pub unsafe extern "C" fn lb_quote_context_warrant_list( let symbol = cstr_to_rust(symbol); let sort_by = sort_by.into(); let sort_order = sort_order.into(); - let warrant_type = std::slice::from_raw_parts(warrant_type, num_warrant_type) + let warrant_type = slice_from_raw_parts(warrant_type, num_warrant_type) .iter() .copied() .map(Into::into) .collect::>(); - let issuer = std::slice::from_raw_parts(issuer, num_issuer).to_vec(); - let expiry_date = std::slice::from_raw_parts(expiry_date, num_expiry_date) + let issuer = slice_from_raw_parts(issuer, num_issuer).to_vec(); + let expiry_date = slice_from_raw_parts(expiry_date, num_expiry_date) .iter() .copied() .map(Into::into) .collect::>(); - let price_type = std::slice::from_raw_parts(price_type, num_price_type) + let price_type = slice_from_raw_parts(price_type, num_price_type) .iter() .copied() .map(Into::into) .collect::>(); - let status = std::slice::from_raw_parts(status, num_status) + let status = slice_from_raw_parts(status, num_status) .iter() .copied() .map(Into::into) @@ -904,7 +907,7 @@ pub unsafe extern "C" fn lb_quote_context_calc_indexes( ) { let ctx_inner = (*ctx).ctx.clone(); let symbols = cstr_array_to_rust(symbols, num_symbols); - let indexes = std::slice::from_raw_parts(indexes, num_indexes) + let indexes = slice_from_raw_parts(indexes, num_indexes) .iter() .map(|index| (*index).into()) .collect::>(); @@ -939,7 +942,7 @@ pub unsafe extern "C" fn lb_quote_context_create_watchlist_group( ) { let ctx_inner = (*ctx).ctx.clone(); let name = cstr_to_rust(req.name); - let securities = std::slice::from_raw_parts(req.securities, req.num_securities); + let securities = slice_from_raw_parts(req.securities, req.num_securities); let securities = (req.num_securities > 0).then(|| { securities .iter() @@ -1003,7 +1006,7 @@ pub unsafe extern "C" fn lb_quote_context_update_watchlist_group( let ctx_inner = (*ctx).ctx.clone(); let id = req.id; let name = ((req.flags & LB_WATCHLIST_GROUP_NAME) != 0).then(|| cstr_to_rust(req.name)); - let securities = std::slice::from_raw_parts(req.securities, req.num_securities); + let securities = slice_from_raw_parts(req.securities, req.num_securities); let securities = ((req.flags & LB_WATCHLIST_GROUP_SECURITIES) != 0).then(|| { securities .iter() diff --git a/c/src/trade_context/context.rs b/c/src/trade_context/context.rs index 40a2e110b..897021a93 100644 --- a/c/src/trade_context/context.rs +++ b/c/src/trade_context/context.rs @@ -32,7 +32,7 @@ use crate::{ CSubmitMultiLegOrderOptions, CSubmitOrderOptions, CSubmitOrderResponseOwned, }, }, - types::{CCow, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust}, + types::{CCow, CVec, ToFFI, cstr_array_to_rust, cstr_to_rust, slice_from_raw_parts}, }; pub type COnOrderChangedCallback = @@ -221,7 +221,7 @@ pub unsafe extern "C" fn lb_trade_context_subscribe( userdata: *mut c_void, ) { let ctx_inner = (*ctx).ctx.clone(); - let topics = std::slice::from_raw_parts(topics, num_topics) + let topics = slice_from_raw_parts(topics, num_topics) .iter() .copied() .map(Into::into) @@ -240,7 +240,7 @@ pub unsafe extern "C" fn lb_trade_context_unsubscribe( userdata: *mut c_void, ) { let ctx_inner = (*ctx).ctx.clone(); - let topics = std::slice::from_raw_parts(topics, num_topics) + let topics = slice_from_raw_parts(topics, num_topics) .iter() .copied() .map(Into::into) @@ -366,7 +366,7 @@ pub unsafe extern "C" fn lb_trade_context_history_orders( opts2 = opts2.symbol(cstr_to_rust((*opts).symbol)); } if !(*opts).status.is_null() { - let status = std::slice::from_raw_parts((*opts).status, (*opts).num_status); + let status = slice_from_raw_parts((*opts).status, (*opts).num_status); opts2 = opts2.status(status.iter().copied().map(Into::into)); } if !(*opts).side.is_null() { @@ -409,7 +409,7 @@ pub unsafe extern "C" fn lb_trade_context_today_orders( opts2 = opts2.symbol(cstr_to_rust((*opts).symbol)); } if !(*opts).status.is_null() { - let status = std::slice::from_raw_parts((*opts).status, (*opts).num_status); + let status = slice_from_raw_parts((*opts).status, (*opts).num_status); opts2 = opts2.status(status.iter().copied().map(Into::into)); } if !(*opts).side.is_null() { @@ -638,7 +638,7 @@ pub unsafe extern "C" fn lb_trade_context_submit_multileg( let order_type = (*opts).order_type.into(); let submitted_quantity = (*(*opts).submitted_quantity).value; let strategy = (*opts).strategy.into(); - let legs = std::slice::from_raw_parts((*opts).legs, (*opts).num_legs) + let legs = slice_from_raw_parts((*opts).legs, (*opts).num_legs) .iter() .map(|leg| { SubmitMultiLegOrderLeg::new(cstr_to_rust(leg.symbol), (*leg.ratio_quantity).value) diff --git a/c/src/types/mod.rs b/c/src/types/mod.rs index 540fab46e..dccae30c4 100644 --- a/c/src/types/mod.rs +++ b/c/src/types/mod.rs @@ -82,8 +82,24 @@ pub(crate) unsafe fn cstr_to_rust(value: *const c_char) -> String { .expect("invalid cstr") } +/// Like [`std::slice::from_raw_parts`], but tolerates a null `data` pointer +/// when `len` is `0`. +/// +/// `from_raw_parts` requires a non-null, aligned pointer even for a zero-length +/// slice, but `std::vector::data()` in C++ is allowed to return `nullptr` for +/// an empty vector — which is exactly what the C++ binding passes for an +/// omitted list argument. Calling `from_raw_parts(null, 0)` is undefined +/// behaviour and aborts the process under the debug UB checks. +pub(crate) unsafe fn slice_from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] { + if len == 0 || data.is_null() { + &[] + } else { + std::slice::from_raw_parts(data, len) + } +} + pub(crate) unsafe fn cstr_array_to_rust(values: *const *const c_char, n: usize) -> Vec { - std::slice::from_raw_parts(values, n) + slice_from_raw_parts(values, n) .iter() .copied() .map(|value| cstr_to_rust(value))