Skip to content

Commit b4fccd2

Browse files
committed
Lint str-ptr-in-c-abi discourage str pointers in C ABI fns
This discourages using a pointer to a `str` instead of a CString for `extern "C"` functions.
1 parent 3f4c5f5 commit b4fccd2

7 files changed

Lines changed: 270 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7456,6 +7456,7 @@ Released 2018-09-13
74567456
[`stable_sort_primitive`]: https://rust-lang.github.io/rust-clippy/master/index.html#stable_sort_primitive
74577457
[`std_instead_of_alloc`]: https://rust-lang.github.io/rust-clippy/master/index.html#std_instead_of_alloc
74587458
[`std_instead_of_core`]: https://rust-lang.github.io/rust-clippy/master/index.html#std_instead_of_core
7459+
[`str_ptr_in_c_abi`]: https://rust-lang.github.io/rust-clippy/master/index.html#str_ptr_in_c_abi
74597460
[`str_split_at_newline`]: https://rust-lang.github.io/rust-clippy/master/index.html#str_split_at_newline
74607461
[`str_to_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#str_to_string
74617462
[`string_add`]: https://rust-lang.github.io/rust-clippy/master/index.html#string_add

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
715715
crate::std_instead_of_core::ALLOC_INSTEAD_OF_CORE_INFO,
716716
crate::std_instead_of_core::STD_INSTEAD_OF_ALLOC_INFO,
717717
crate::std_instead_of_core::STD_INSTEAD_OF_CORE_INFO,
718+
crate::str_ptr_in_c_abi::STR_PTR_IN_C_ABI_INFO,
718719
crate::string_patterns::MANUAL_PATTERN_CHAR_COMPARISON_INFO,
719720
crate::string_patterns::SINGLE_CHAR_PATTERN_INFO,
720721
crate::strings::STR_TO_STRING_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ mod size_of_in_element_count;
349349
mod size_of_ref;
350350
mod slow_vector_initialization;
351351
mod std_instead_of_core;
352+
mod str_ptr_in_c_abi;
352353
mod string_patterns;
353354
mod strings;
354355
mod strlen_on_c_strings;
@@ -868,6 +869,7 @@ rustc_lint::late_lint_methods!(
868869
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
869870
RestWhenDestructuringStruct: rest_when_destructuring_struct::RestWhenDestructuringStruct = rest_when_destructuring_struct::RestWhenDestructuringStruct,
870871
BlockScrutinee: block_scrutinee::BlockScrutinee = block_scrutinee::BlockScrutinee,
872+
StrPtrInCAbi: str_ptr_in_c_abi::StrPtrInCAbi = str_ptr_in_c_abi::StrPtrInCAbi,
871873
// add late passes here, used by `cargo dev new_lint`
872874
]]
873875
);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use rustc_hir::{Expr, ExprKind, TyKind};
2+
use rustc_lint::{LateContext, LateLintPass};
3+
use rustc_session::declare_lint_pass;
4+
5+
use clippy_utils::diagnostics::span_lint_and_help;
6+
use clippy_utils::sym;
7+
8+
declare_clippy_lint! {
9+
/// ### What it does
10+
///
11+
/// This lint triggers if a pointer to a Rust `str` is passed into an `extern "C"` interface
12+
/// where you should instead be providing a pointer to a `CString`.
13+
///
14+
/// ### Why is this bad?
15+
///
16+
/// Foreign functions under the C ABI expect that a string ends with a null byte (`'\0'`).
17+
/// Rust's `str` doesn't provide a null byte. Instead it contains a length for the string.
18+
/// Without a null byte, foreign functions can read beyond the memory allocated to the string searching for the null terminator, causing undefined behavior (UB).
19+
///
20+
/// ### Example
21+
/// ```no_run
22+
/// # unsafe extern "C" fn strlen(s: *const i8) -> usize { unimplemented!() }
23+
/// unsafe { strlen("Hello".as_ptr() as *const _) };
24+
/// ```
25+
/// Use instead:
26+
/// ```no_run
27+
/// # unsafe extern "C" fn strlen(s: *const i8) -> usize { unimplemented!() }
28+
/// let cstring = std::ffi::CString::new("Hello".as_bytes()).unwrap();
29+
/// unsafe { strlen(cstring.as_ptr()) };
30+
/// ```
31+
#[clippy::version = "1.99.0"]
32+
pub STR_PTR_IN_C_ABI,
33+
suspicious,
34+
"discourage str pointers in C ABI fns"
35+
}
36+
37+
declare_lint_pass!(StrPtrInCAbi => [STR_PTR_IN_C_ABI]);
38+
39+
impl<'tcx> LateLintPass<'tcx> for StrPtrInCAbi {
40+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
41+
// find call expressions
42+
if let ExprKind::Call(callee, args) = expr.kind
43+
// where the signature of the callee shows it is an `extern "C" fn`
44+
&& is_extern_c_fn(cx, callee)
45+
// and the fn is given a str pointer as argument
46+
&& let spans = args
47+
.iter()
48+
.filter(|arg| is_str_ptr(cx, arg))
49+
.map(|arg| arg.span)
50+
.collect::<Vec<_>>()
51+
&& !spans.is_empty()
52+
{
53+
span_lint_and_help(
54+
cx,
55+
STR_PTR_IN_C_ABI,
56+
spans,
57+
"giving a pointer to a Rust `str` to an `extern \"C\" fn` can cause undefined behavior",
58+
/* help_span */ None,
59+
"if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that",
60+
);
61+
}
62+
}
63+
}
64+
65+
/// Does the expression represent an `extern "C" fn` of some type?
66+
fn is_extern_c_fn<'tcx>(cx: &LateContext<'tcx>, callee: &'tcx Expr<'tcx>) -> bool {
67+
let callee_ty = cx.typeck_results().expr_ty_adjusted(callee);
68+
if !(callee_ty.is_fn() || callee_ty.is_fn_ptr()) {
69+
return false;
70+
}
71+
// NOTE: fn_sig panics if callee_ty isn't a function, but the above return ensures that it is
72+
matches!(callee_ty.fn_sig(cx.tcx).abi(), rustc_abi::ExternAbi::C { .. })
73+
}
74+
75+
/// Is `arg` shaped like `derefs_to_str.as_ptr() as *const _`, maybe without the cast?
76+
fn is_str_ptr<'tcx>(cx: &LateContext<'tcx>, arg: &Expr<'tcx>) -> bool {
77+
let typeck = cx.typeck_results();
78+
match arg.kind {
79+
// NOTE: recursion is bounded by how many nested casts to ptr the user does
80+
ExprKind::Cast(expr, ty) if matches!(ty.kind, TyKind::Ptr(_)) => is_str_ptr(cx, expr),
81+
ExprKind::MethodCall(method, this, _args, _span) => {
82+
matches!(method.ident.name, sym::as_ptr | sym::as_mut_ptr)
83+
&& typeck.expr_ty_adjusted(this).peel_refs().is_str()
84+
},
85+
_ => false,
86+
}
87+
}

clippy_utils/src/sym.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ generate! {
138138
as_deref,
139139
as_deref_mut,
140140
as_mut,
141+
as_mut_ptr,
141142
as_path,
142143
as_ptr,
143144
as_str,

tests/ui/str_ptr_in_c_abi.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#![warn(clippy::str_ptr_in_c_abi)]
2+
3+
use std::ffi::CString;
4+
5+
fn main() {
6+
// This should use a pointer to a CString
7+
unsafe { printf("Hello".as_ptr() as *const _) };
8+
//~^ str_ptr_in_c_abi
9+
10+
// Like this
11+
let cstring = CString::new("Hello".as_bytes()).unwrap();
12+
unsafe { printf(cstring.as_ptr()) };
13+
14+
// One should also use mut pointers to CStrings
15+
let mut buffer = String::new();
16+
let mut buffer = buffer.as_mut_str(); // this lint can only detect `str`s for now
17+
unsafe { strcpy(buffer.as_mut_ptr() as *mut _, cstring.as_ptr()) };
18+
//~^ str_ptr_in_c_abi
19+
20+
let mut cstring_mut = CString::new([]).unwrap();
21+
unsafe { strcpy(cstring_mut.into_raw(), cstring.as_ptr()) };
22+
23+
// Two rust strings at once!
24+
unsafe { strcpy(buffer.as_mut_ptr() as *mut _, "Hello".as_ptr() as *const _) };
25+
//~^ str_ptr_in_c_abi
26+
27+
// It can detect smart pointers to str
28+
let hello_string: String = "Hello".into();
29+
let hello_box: Box<str> = "Hello".into();
30+
let hello_rc: std::rc::Rc<str> = "Hello".into();
31+
unsafe { printf(hello_string.as_ptr() as *const _) };
32+
//~^ str_ptr_in_c_abi
33+
unsafe { printf(hello_box.as_ptr() as *const _) };
34+
//~^ str_ptr_in_c_abi
35+
unsafe { printf(hello_rc.as_ptr() as *const _) };
36+
//~^ str_ptr_in_c_abi
37+
38+
// It detects str to ptr casts in variadics
39+
let fmt = CString::new("%s\n".as_bytes()).unwrap();
40+
unsafe { printf(fmt.as_ptr(), "I'm (incorrectly) printf-ing a str!".as_ptr() as *const _) };
41+
//~^ str_ptr_in_c_abi
42+
43+
// The cast isn't necessary to trigger the lint, which is relevant for not type-checked variadics
44+
unsafe { printf(fmt.as_ptr(), "hello".as_ptr()) };
45+
//~^ str_ptr_in_c_abi
46+
47+
// It detects the str ptr through multiple casts
48+
unsafe { printf("hello".as_ptr() as *const _ as *const _) };
49+
//~^ str_ptr_in_c_abi
50+
unsafe { printf("hello".as_ptr() as *const _ as *const _ as *const _) };
51+
//~^ str_ptr_in_c_abi
52+
53+
// it can see inside of smart pointers to functions
54+
let printf_box: Box<unsafe extern "C" fn(*const i8, ...) -> i32> = Box::new(printf);
55+
let printf_rc: std::rc::Rc<unsafe extern "C" fn(*const i8, ...) -> i32> = std::rc::Rc::new(printf);
56+
unsafe { printf_box("hello".as_ptr() as *const _) };
57+
//~^ str_ptr_in_c_abi
58+
unsafe { printf_rc("hello".as_ptr() as *const _) };
59+
//~^ str_ptr_in_c_abi
60+
#[allow(clippy::needless_borrow)]
61+
unsafe {
62+
(&printf)("hello".as_ptr() as *const _)
63+
//~^ str_ptr_in_c_abi
64+
};
65+
}
66+
67+
unsafe extern "C" {
68+
fn strcpy(dst: *mut i8, src: *const i8) -> *mut i8;
69+
fn printf(format: *const i8, ...) -> i32;
70+
}

tests/ui/str_ptr_in_c_abi.stderr

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
2+
--> tests/ui/str_ptr_in_c_abi.rs:7:21
3+
|
4+
LL | unsafe { printf("Hello".as_ptr() as *const _) };
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
8+
= note: `-D clippy::str-ptr-in-c-abi` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::str_ptr_in_c_abi)]`
10+
11+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
12+
--> tests/ui/str_ptr_in_c_abi.rs:17:21
13+
|
14+
LL | unsafe { strcpy(buffer.as_mut_ptr() as *mut _, cstring.as_ptr()) };
15+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16+
|
17+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
18+
19+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
20+
--> tests/ui/str_ptr_in_c_abi.rs:24:21
21+
|
22+
LL | unsafe { strcpy(buffer.as_mut_ptr() as *mut _, "Hello".as_ptr() as *const _) };
23+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24+
|
25+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
26+
27+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
28+
--> tests/ui/str_ptr_in_c_abi.rs:31:21
29+
|
30+
LL | unsafe { printf(hello_string.as_ptr() as *const _) };
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
|
33+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
34+
35+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
36+
--> tests/ui/str_ptr_in_c_abi.rs:33:21
37+
|
38+
LL | unsafe { printf(hello_box.as_ptr() as *const _) };
39+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
|
41+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
42+
43+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
44+
--> tests/ui/str_ptr_in_c_abi.rs:35:21
45+
|
46+
LL | unsafe { printf(hello_rc.as_ptr() as *const _) };
47+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48+
|
49+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
50+
51+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
52+
--> tests/ui/str_ptr_in_c_abi.rs:40:35
53+
|
54+
LL | unsafe { printf(fmt.as_ptr(), "I'm (incorrectly) printf-ing a str!".as_ptr() as *const _) };
55+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
|
57+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
58+
59+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
60+
--> tests/ui/str_ptr_in_c_abi.rs:44:35
61+
|
62+
LL | unsafe { printf(fmt.as_ptr(), "hello".as_ptr()) };
63+
| ^^^^^^^^^^^^^^^^
64+
|
65+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
66+
67+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
68+
--> tests/ui/str_ptr_in_c_abi.rs:48:21
69+
|
70+
LL | unsafe { printf("hello".as_ptr() as *const _ as *const _) };
71+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
|
73+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
74+
75+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
76+
--> tests/ui/str_ptr_in_c_abi.rs:50:21
77+
|
78+
LL | unsafe { printf("hello".as_ptr() as *const _ as *const _ as *const _) };
79+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80+
|
81+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
82+
83+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
84+
--> tests/ui/str_ptr_in_c_abi.rs:56:25
85+
|
86+
LL | unsafe { printf_box("hello".as_ptr() as *const _) };
87+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88+
|
89+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
90+
91+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
92+
--> tests/ui/str_ptr_in_c_abi.rs:58:24
93+
|
94+
LL | unsafe { printf_rc("hello".as_ptr() as *const _) };
95+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96+
|
97+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
98+
99+
error: giving a pointer to a Rust `str` to an `extern "C" fn` can cause undefined behavior
100+
--> tests/ui/str_ptr_in_c_abi.rs:62:19
101+
|
102+
LL | (&printf)("hello".as_ptr() as *const _)
103+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
104+
|
105+
= help: if the foreign function calls for a null-terminated string in this position, first convert this value to a `std::ffi::CString` and take the pointer from that
106+
107+
error: aborting due to 13 previous errors
108+

0 commit comments

Comments
 (0)