-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Lint str-ptr-in-c-abi discourage str pointers in C ABI fns #17401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||
| use rustc_hir::{Expr, ExprKind, TyKind}; | ||||||
| use rustc_lint::{LateContext, LateLintPass}; | ||||||
| use rustc_session::declare_lint_pass; | ||||||
|
|
||||||
| use clippy_utils::diagnostics::span_lint_and_help; | ||||||
| use clippy_utils::sym; | ||||||
|
|
||||||
| declare_clippy_lint! { | ||||||
| /// ### What it does | ||||||
| /// | ||||||
| /// This lint triggers if a pointer to a Rust `str` is passed into an `extern "C"` interface | ||||||
| /// where you should instead be providing a pointer to a `CString`. | ||||||
| /// | ||||||
| /// ### Why is this bad? | ||||||
| /// | ||||||
| /// Foreign functions under the C ABI expect that a string ends with a null byte (`'\0'`). | ||||||
| /// Rust's `str` doesn't provide a null byte. Instead it contains a length for the string. | ||||||
| /// This leads to two problems. | ||||||
| /// | ||||||
| /// 1. The length parameter of the Rust string will be misinterpreted as a character, which is logically invalid. | ||||||
| /// 2. Without a null byte, foreign functions will read beyond the memory allocated to the string searching for the null terminator, causing undefined behavior (UB). | ||||||
| /// | ||||||
| /// ### Example | ||||||
| /// ```no_run | ||||||
| /// # unsafe extern "C" fn strlen(s: *const i8) -> usize { unimplemented!() } | ||||||
| /// unsafe { strlen("Hello".as_ptr() as *const _) }; | ||||||
| /// ``` | ||||||
| /// Use instead: | ||||||
| /// ```no_run | ||||||
| /// # unsafe extern "C" fn strlen(s: *const i8) -> usize { unimplemented!() } | ||||||
| /// let cstring = std::ffi::CString::new("Hello".as_bytes()).unwrap(); | ||||||
| /// unsafe { strlen(cstring.as_ptr()) }; | ||||||
| /// ``` | ||||||
| #[clippy::version = "1.99.0"] | ||||||
| pub STR_PTR_IN_C_ABI, | ||||||
| nursery, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should choose a category for this lint, I would personally go with suspicious. |
||||||
| "discourage str pointers in C ABI fns" | ||||||
| } | ||||||
|
|
||||||
| declare_lint_pass!(StrPtrInCAbi => [STR_PTR_IN_C_ABI]); | ||||||
|
|
||||||
| impl<'tcx> LateLintPass<'tcx> for StrPtrInCAbi { | ||||||
| fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | ||||||
| // find call expressions | ||||||
| if let ExprKind::Call(callee, args) = expr.kind | ||||||
| // where the signature of the callee shows it is | ||||||
| // extern "C" fn | ||||||
| && is_extern_c_fn(cx, callee) | ||||||
| // and takes a raw pointer as an argument | ||||||
| // and to that argument gives a pointer to a Rust `str` | ||||||
| && let span = args | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
|
||||||
| .iter() | ||||||
| .filter(|arg| is_cast_str_ptr_to_raw_ptr(cx, arg)) | ||||||
| .map(|arg| arg.span) | ||||||
| .collect::<Vec<_>>() | ||||||
| && !span.is_empty() | ||||||
| { | ||||||
| span_lint_and_help( | ||||||
| cx, | ||||||
| STR_PTR_IN_C_ABI, | ||||||
| span, | ||||||
| "giving a pointer to a Rust `str` to an `extern \"C\" fn` can cause undefined behavior", | ||||||
| /* help_span */ None, | ||||||
| "first convert the `str` to a `std::ffi::CString` and then get a pointer from there", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be softened. Some FFI APIs take a pointer plus a length, so we can't always prove that converting to Something like ""if this function expects a nul-terminated string, convert the |
||||||
| ); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// Does the expression represent an `extern "C" fn` of some type? | ||||||
| fn is_extern_c_fn<'tcx>(cx: &LateContext<'tcx>, callee: &'tcx Expr<'tcx>) -> bool { | ||||||
| let callee_ty = cx.typeck_results().expr_ty(callee); | ||||||
| if !(callee_ty.is_fn() || callee_ty.is_fn_ptr()) { | ||||||
| return false; | ||||||
| } | ||||||
| matches!(callee_ty.fn_sig(cx.tcx).abi(), rustc_abi::ExternAbi::C { .. }) | ||||||
| } | ||||||
|
|
||||||
| /// If `arg` is `derefs_to_str.as(_mut)_ptr() as *const(mut) _`, then true. | ||||||
| fn is_cast_str_ptr_to_raw_ptr<'tcx>(cx: &LateContext<'tcx>, arg: &Expr<'tcx>) -> bool { | ||||||
| let typeck = cx.typeck_results(); | ||||||
| if let ExprKind::Cast(expr, ty) = arg.kind | ||||||
| && matches!(ty.kind, TyKind::Ptr(_)) | ||||||
| && let ExprKind::MethodCall(method, this, _args, _span) = expr.kind | ||||||
| && matches!(method.ident.name, sym::as_ptr | sym::as_mut_ptr) | ||||||
| && typeck.expr_ty_adjusted(this).peel_refs().is_str() | ||||||
| { | ||||||
| return true; | ||||||
| } | ||||||
| false | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,6 +138,7 @@ generate! { | |
| as_deref, | ||
| as_deref_mut, | ||
| as_mut, | ||
| as_mut_ptr, | ||
| as_path, | ||
| as_ptr, | ||
| as_str, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #![warn(clippy::str_ptr_in_c_abi)] | ||
|
|
||
| use std::ffi::CString; | ||
|
|
||
| fn main() { | ||
| // This should use a pointer to a CString | ||
| unsafe { printf("Hello".as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
|
|
||
| // Like this | ||
| let cstring = CString::new("Hello".as_bytes()).unwrap(); | ||
| unsafe { printf(cstring.as_ptr()) }; | ||
|
|
||
| // One should also use mut pointers to CStrings | ||
| let mut buffer = String::new(); | ||
| let mut buffer = buffer.as_mut_str(); // this lint can only detect `str`s for now | ||
| unsafe { strcpy(buffer.as_mut_ptr() as *mut _, cstring.as_ptr()) }; | ||
| //~^ str_ptr_in_c_abi | ||
|
|
||
| let mut cstring_mut = CString::new([]).unwrap(); | ||
| unsafe { strcpy(cstring_mut.into_raw(), cstring.as_ptr()) }; | ||
|
|
||
| // Two rust strings at once! | ||
| unsafe { strcpy(buffer.as_mut_ptr() as *mut _, "Hello".as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
|
|
||
| // It can detect smart pointers to str | ||
| let hello_string: String = "Hello".into(); | ||
| let hello_box: Box<str> = "Hello".into(); | ||
| let hello_rc: std::rc::Rc<str> = "Hello".into(); | ||
| unsafe { printf(hello_string.as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
| unsafe { printf(hello_box.as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
| unsafe { printf(hello_rc.as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
|
|
||
| // It detects str to ptr casts in variadics | ||
| let fmt = CString::new("%s\n".as_bytes()).unwrap(); | ||
| unsafe { printf(fmt.as_ptr(), "I'm (incorrectly) printf-ing a str!".as_ptr() as *const _) }; | ||
| //~^ str_ptr_in_c_abi | ||
| } | ||
|
|
||
| unsafe extern "C" { | ||
| fn strcpy(dst: *mut i8, src: *const i8) -> *mut i8; | ||
| fn printf(format: *const i8, ...) -> i32; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:7:21 | ||
| | | ||
| LL | unsafe { printf("Hello".as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
| = note: `-D clippy::str-ptr-in-c-abi` implied by `-D warnings` | ||
| = help: to override `-D warnings` add `#[allow(clippy::str_ptr_in_c_abi)]` | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:17:21 | ||
| | | ||
| LL | unsafe { strcpy(buffer.as_mut_ptr() as *mut _, cstring.as_ptr()) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:24:21 | ||
| | | ||
| LL | unsafe { strcpy(buffer.as_mut_ptr() as *mut _, "Hello".as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:31:21 | ||
| | | ||
| LL | unsafe { printf(hello_string.as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:33:21 | ||
| | | ||
| LL | unsafe { printf(hello_box.as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:35:21 | ||
| | | ||
| LL | unsafe { printf(hello_rc.as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior | ||
| --> tests/ui/str_ptr_in_c_abi.rs:40:35 | ||
| | | ||
| LL | unsafe { printf(fmt.as_ptr(), "I'm (incorrectly) printf-ing a str!".as_ptr() as *const _) }; | ||
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| | | ||
| = help: first convert the `str` to a `std::ffi::CString` and then get a pointer from there | ||
|
|
||
| error: aborting due to 7 previous errors | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect, the C function will not see or misinterpret rust length metadata as a character since
str::as_ptr()only returns a pointer to the string’s UTF-8 bytes. Better to just remove this since the main problem is that C APIs can read past the string if no length is provided.