Skip to content

Commit 2ba14bd

Browse files
oslfmtsmoelius
authored andcommitted
add anchor check to type-cosplay
1 parent 017fc41 commit 2ba14bd

File tree

12 files changed

+2975
-31
lines changed

12 files changed

+2975
-31
lines changed

crate/src/paths.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub const ANCHOR_LANG_SIGNER: [&str; 4] = ["anchor_lang", "accounts", "signer",
44
pub const SOLANA_PROGRAM_ACCOUNT_INFO: [&str; 3] =
55
["solana_program", "account_info", "AccountInfo"];
66
pub const BORSH_TRY_FROM_SLICE: [&str; 4] = ["borsh", "de", "BorshDeserialize", "try_from_slice"];
7+

lints/type_cosplay/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ path = "ui/insecure-2/src/lib.rs"
2121
name = "insecure-3"
2222
path = "ui/insecure-3/src/lib.rs"
2323

24+
[[example]]
25+
name = "insecure-anchor"
26+
path = "ui/insecure-anchor/src/lib.rs"
27+
2428
[[example]]
2529
name = "recommended"
2630
path = "ui/recommended/src/lib.rs"

lints/type_cosplay/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ and, if it deserializes the same as the first enum, then this may be a possible
3131

3232
Furthermore, one may have alternative definitions of a discriminant, such as using a bool,
3333
or u8, and not an enum. This will flag a false positive.
34+
35+
## Note on Tests
36+
**insecure-anchor**: insecure because `User` type derives Discriminator trait (via `#[account]`),
37+
thus one may expect this code to be secure. However, the program tries to deserialize with
38+
`try_from_slice`, the default borsh deserialization method, which does _not_ check for the
39+
discriminator. Thus, one could potentially serialize a `Metadata` struct, and then later
40+
deserialize without any problem into a `User` struct, leading to a type-cosplay vulnerability.
41+
42+
**recommended**: this is secure code because all structs have an `#[account]` macro attributed
43+
on them, thus deriving the `Discriminator` trait for each. Further, unlike the insecure-anchor
44+
example, the program uses the proper deserialization method, `try_deserialize`, to deserialize
45+
bytes as `User`. This is "proper" because in the derived implementation of `try_deserialize`,
46+
the discriminator of the type is checked first.

lints/type_cosplay/src/lib.rs

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
#![feature(rustc_private)]
22
#![warn(unused_extern_crates)]
3+
#![recursion_limit = "256"]
34

45
extern crate rustc_data_structures;
56
extern crate rustc_hir;
67
extern crate rustc_index;
78
extern crate rustc_middle;
89
extern crate rustc_span;
910

10-
use clippy_utils::{diagnostics::span_lint_and_help, match_def_path, ty::match_type};
11+
use clippy_utils::{diagnostics::span_lint_and_help, match_def_path, ty::implements_trait, get_trait_def_id};
1112
use rustc_data_structures::fx::FxHashMap;
1213
use rustc_hir::{def::Res, Expr, ExprKind, QPath, TyKind};
1314
use rustc_index::vec::Idx;
1415
use rustc_lint::{LateContext, LateLintPass};
1516
use rustc_middle::ty::{AdtDef, AdtKind, TyKind as MiddleTyKind};
1617
use rustc_span::{def_id::DefId, Span};
17-
use solana_lints::{paths, utils::visit_expr_no_bodies};
18+
use solana_lints::paths;
1819

1920
use if_chain::if_chain;
2021

@@ -61,31 +62,58 @@ struct TypeCosplay {
6162
deser_types: FxHashMap<AdtKind, Vec<(DefId, Span)>>,
6263
}
6364

65+
// get type X
66+
// check if implements Discriminator
67+
// check corresponding function call type:
68+
// if !try_deserialize
69+
// emit lint with warning to use try_deserialize (because any types deriving Discriminator should since it guaranteed to check discrim)
70+
6471
impl<'tcx> LateLintPass<'tcx> for TypeCosplay {
6572
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
6673
if_chain! {
67-
if !expr.span.from_expansion();
68-
if let ExprKind::Call(fnc_expr, args_exprs) = expr.kind;
69-
if is_deserialize_function(cx, fnc_expr);
74+
if let ExprKind::Call(fnc_expr, _args_exprs) = expr.kind;
75+
// TODO: is the following if statement really needed?? don't think it's ever used.
76+
// all it does is check if AccountInfo.data is referenced...but what bytes we deser
77+
// from shouldn't impact if this is a type-cosplay issue or not.
7078
// walk each argument expression and see if the data field is referenced
71-
if args_exprs.iter().any(|arg| {
72-
visit_expr_no_bodies(arg, |expr| contains_data_field_reference(cx, expr))
73-
});
79+
// if args_exprs.iter().any(|arg| {
80+
// visit_expr_no_bodies(arg, |expr| contains_data_field_reference(cx, expr))
81+
// });
7482
// get the type that the function was called on, ie X in X::deser()
7583
if let ExprKind::Path(qpath) = &fnc_expr.kind;
7684
if let QPath::TypeRelative(ty, _) = qpath;
7785
if let TyKind::Path(ty_qpath) = &ty.kind;
7886
let res = cx.typeck_results().qpath_res(ty_qpath, ty.hir_id);
7987
if let Res::Def(_, def_id) = res;
88+
let middle_ty = cx.tcx.type_of(def_id);
89+
if let Some(trait_did) = get_trait_def_id(cx, &paths::ANCHOR_DISCRIMINATOR_TRAIT);
8090
then {
81-
let middle_ty = cx.tcx.type_of(def_id);
82-
if let MiddleTyKind::Adt(adt_def, _) = middle_ty.kind() {
83-
let adt_kind = adt_def.adt_kind();
84-
let def_id = adt_def.did();
85-
if let Some(vec) = self.deser_types.get_mut(&adt_kind) {
86-
vec.push((def_id, ty.span));
87-
} else {
88-
self.deser_types.insert(adt_kind, vec![(def_id, ty.span)]);
91+
if implements_trait(cx, middle_ty, trait_did, &[]) {
92+
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(fnc_expr.hir_id) {
93+
if !match_def_path(cx, def_id, &paths::ANCHOR_TRY_DESERIALIZE) {
94+
span_lint_and_help(
95+
cx,
96+
TYPE_COSPLAY,
97+
fnc_expr.span,
98+
&format!("{} type implements the anchor_lang::Discriminator trait. If you are using #[account] to derive Discriminator, use try_deserialize() instead.",
99+
middle_ty),
100+
None,
101+
"otherwise, make sure you are accounting for this type's discriminator in your deserialization function"
102+
);
103+
}
104+
}
105+
} else {
106+
// currently only checks borsh::try_from_slice()
107+
if is_deserialize_function(cx, fnc_expr) {
108+
if let MiddleTyKind::Adt(adt_def, _) = middle_ty.kind() {
109+
let adt_kind = adt_def.adt_kind();
110+
let def_id = adt_def.did();
111+
if let Some(vec) = self.deser_types.get_mut(&adt_kind) {
112+
vec.push((def_id, ty.span));
113+
} else {
114+
self.deser_types.insert(adt_kind, vec![(def_id, ty.span)]);
115+
}
116+
}
89117
}
90118
}
91119
}
@@ -126,19 +154,19 @@ fn is_deserialize_function(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
126154
}
127155
}
128156

129-
fn contains_data_field_reference(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
130-
if_chain! {
131-
if let ExprKind::Field(obj_expr, ident) = expr.kind;
132-
if ident.as_str() == "data";
133-
let ty = cx.typeck_results().expr_ty(obj_expr);
134-
if match_type(cx, ty, &paths::SOLANA_PROGRAM_ACCOUNT_INFO);
135-
then {
136-
true
137-
} else {
138-
false
139-
}
140-
}
141-
}
157+
// fn contains_data_field_reference(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
158+
// if_chain! {
159+
// if let ExprKind::Field(obj_expr, ident) = expr.kind;
160+
// if ident.as_str() == "data";
161+
// let ty = cx.typeck_results().expr_ty(obj_expr);
162+
// if match_type(cx, ty, &paths::SOLANA_PROGRAM_ACCOUNT_INFO);
163+
// then {
164+
// true
165+
// } else {
166+
// false
167+
// }
168+
// }
169+
// }
142170

143171
fn check_enums(cx: &LateContext<'_>, enums: &Vec<(DefId, Span)>) {
144172
#[allow(clippy::comparison_chain)]
@@ -188,7 +216,7 @@ fn has_discriminant(cx: &LateContext, adt: AdtDef, num_struct_types: usize, span
188216
"type does not have a proper discriminant. It may be indistinguishable when deserialized.",
189217
None,
190218
"add an enum with at least as many variants as there are struct definitions"
191-
)
219+
);
192220
}
193221
}
194222
}
@@ -208,6 +236,11 @@ fn insecure_3() {
208236
dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "insecure-3");
209237
}
210238

239+
#[test]
240+
fn insecure_anchor() {
241+
dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "insecure-anchor");
242+
}
243+
211244
#[test]
212245
fn secure() {
213246
dylint_testing::ui_test_example(env!("CARGO_PKG_NAME"), "secure");

0 commit comments

Comments
 (0)