fix(ffi): reject a null stream callback at registration#825
Merged
Conversation
A C caller can pass a null function pointer to the set-callback entries. The extern "C" fn type is non-nullable in Rust's model, so the null was stored and later invoked on the dispatcher thread when the first event arrived, dereferencing address 0 as a process-level fault the unwind boundary cannot contain. Model the parameter as Option<ThetaDataDxStreamCallback> so the null bit pattern is representable and reject it up front with a recoverable error code. The C ABI repr is unchanged (the null niche), so the header and exported symbol are identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
thetadatadx_client_set_callbackandthetadatadx_streaming_set_callbacktyped the callback as a bareextern "C" fn, which Rust treats as non-nullable. A C / Go consumer can still passNULL(the C ABI permits0). The null was stored and then invoked on the dispatcher thread when the first event arrived — a call through address0that faults the process and cannot be contained by thecatch_unwindboundary. It passes a register-and-return smoke test and crashes in production on the first event. This is the only caller pointer that is invoked rather than read, and was the lone pointer arg with no null guard.Fix
Model the parameter as
Option<ThetaDataDxStreamCallback>and rejectNoneat registration with a normal-1+thetadatadx_last_error()message.Option<extern "C" fn>uses the null niche, so the C ABI representation, the public header, and the exported symbol are byte-identical — no consumer change. (The compiler rejects a rawas *const ()).is_null()check asuseless-ptr-null-checksprecisely because the bare fn type is assumed non-null;Optionis the idiomatic, lint-clean expression.)Test
cargo test -p thetadatadx-ffi --lib null_callback— pins theNone-niche contract the guards depend on. C-ABI completeness check clean (325 symbols, bidirectional parity);cargo clippy -p thetadatadx-ffi --all-targets -- -D warningsclean.🤖 Generated with Claude Code