Skip to content

Commit 885f8ba

Browse files
authored
lang: Error on undocumented unsafe account types (otter-sec#1452)
1 parent 6baad93 commit 885f8ba

40 files changed

Lines changed: 342 additions & 7 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ jobs:
278278
path: tests/auction-house
279279
- cmd: cd tests/floats && yarn && anchor test
280280
path: tests/floats
281+
- cmd: cd tests/safety-checks && ./test.sh
282+
path: tests/safety-checks
281283
steps:
282284
- uses: actions/checkout@v2
283285
- uses: ./.github/actions/setup/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ incremented for features.
2424

2525
* lang: Enforce that the payer for an init-ed account be marked `mut` ([#1271](https://github.com/project-serum/anchor/pull/1271)).
2626
* lang: All error-related code is now in the error module ([#1426](https://github.com/project-serum/anchor/pull/1426)).
27+
* lang: Require doc comments when using AccountInfo or UncheckedAccount types ([#1452](https://github.com/project-serum/anchor/pull/1452)).
2728

2829
## [0.21.0] - 2022-02-07
2930

cli/src/config.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ impl WithPath<Config> {
166166
path.join("src/lib.rs"),
167167
version,
168168
self.features.seeds,
169+
false,
169170
)?;
170171
r.push(Program {
171172
lib_name,
@@ -256,9 +257,26 @@ pub struct Config {
256257
pub test: Option<Test>,
257258
}
258259

259-
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
260+
#[derive(Clone, Debug, Serialize, Deserialize)]
260261
pub struct FeaturesConfig {
262+
#[serde(default)]
261263
pub seeds: bool,
264+
#[serde(default = "default_safety_checks")]
265+
pub safety_checks: bool,
266+
}
267+
268+
impl Default for FeaturesConfig {
269+
fn default() -> Self {
270+
Self {
271+
seeds: false,
272+
// Anchor safety checks on by default
273+
safety_checks: true,
274+
}
275+
}
276+
}
277+
278+
fn default_safety_checks() -> bool {
279+
true
262280
}
263281

264282
#[derive(Clone, Debug, Serialize, Deserialize)]

cli/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,12 @@ fn extract_idl(cfg: &WithPath<Config>, file: &str) -> Result<Option<Idl>> {
13881388
let manifest_from_path = std::env::current_dir()?.join(PathBuf::from(&*file).parent().unwrap());
13891389
let cargo = Manifest::discover_from_path(manifest_from_path)?
13901390
.ok_or_else(|| anyhow!("Cargo.toml not found"))?;
1391-
anchor_syn::idl::file::parse(&*file, cargo.version(), cfg.features.seeds)
1391+
anchor_syn::idl::file::parse(
1392+
&*file,
1393+
cargo.version(),
1394+
cfg.features.seeds,
1395+
cfg.features.safety_checks,
1396+
)
13921397
}
13931398

13941399
fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> {

lang/syn/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ anchor-debug = []
1616
seeds = []
1717

1818
[dependencies]
19-
proc-macro2 = "1.0"
19+
proc-macro2 = { version = "1.0", features=["span-locations"]}
2020
proc-macro2-diagnostics = "0.9"
2121
quote = "1.0"
2222
syn = { version = "1.0.60", features = ["full", "extra-traits", "parsing"] }

lang/syn/src/idl/file.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ pub fn parse(
1818
filename: impl AsRef<Path>,
1919
version: String,
2020
seeds_feature: bool,
21+
safety_checks: bool,
2122
) -> Result<Option<Idl>> {
2223
let ctx = CrateContext::parse(filename)?;
24+
if safety_checks {
25+
ctx.safety_checks()?;
26+
}
2327

2428
let program_mod = match parse_program_mod(&ctx) {
2529
None => return Ok(None),

lang/syn/src/parser/context.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
use anyhow::anyhow;
12
use std::collections::BTreeMap;
23
use std::path::{Path, PathBuf};
3-
44
use syn::parse::{Error as ParseError, Result as ParseResult};
55

66
/// Crate parse context
@@ -40,6 +40,41 @@ impl CrateContext {
4040
modules: ParsedModule::parse_recursive(root.as_ref())?,
4141
})
4242
}
43+
44+
// Perform Anchor safety checks on the parsed create
45+
pub fn safety_checks(&self) -> Result<(), anyhow::Error> {
46+
// Check all structs for unsafe field types, i.e. AccountInfo and UncheckedAccount.
47+
for (_, ctx) in self.modules.iter() {
48+
for unsafe_field in ctx.unsafe_struct_fields() {
49+
// Check if unsafe field type has been documented with a /// SAFETY: doc string.
50+
let is_documented = unsafe_field.attrs.iter().any(|attr| {
51+
attr.tokens.clone().into_iter().any(|token| match token {
52+
// Check for doc comments containing CHECK
53+
proc_macro2::TokenTree::Literal(s) => s.to_string().contains("CHECK"),
54+
_ => false,
55+
})
56+
});
57+
if !is_documented {
58+
let ident = unsafe_field.ident.as_ref().unwrap();
59+
let span = ident.span();
60+
// Error if undocumented.
61+
return Err(anyhow!(
62+
r#"
63+
{}:{}:{}
64+
Struct field "{}" is unsafe, but is not documented.
65+
Please add a `/// CHECK:` doc comment explaining why no checks through types are necessary.
66+
See https://book.anchor-lang.com/chapter_3/the_accounts_struct.html#safety-checks for more information.
67+
"#,
68+
ctx.file.canonicalize().unwrap().display(),
69+
span.start().line,
70+
span.start().column,
71+
ident.to_string()
72+
));
73+
};
74+
}
75+
}
76+
Ok(())
77+
}
4378
}
4479

4580
/// Module parse context
@@ -181,6 +216,21 @@ impl ParsedModule {
181216
})
182217
}
183218

219+
fn unsafe_struct_fields(&self) -> impl Iterator<Item = &syn::Field> {
220+
self.structs()
221+
.flat_map(|s| &s.fields)
222+
.filter(|f| match &f.ty {
223+
syn::Type::Path(syn::TypePath {
224+
path: syn::Path { segments, .. },
225+
..
226+
}) => {
227+
segments.len() == 1 && segments[0].ident == "UncheckedAccount"
228+
|| segments[0].ident == "AccountInfo"
229+
}
230+
_ => false,
231+
})
232+
}
233+
184234
fn enums(&self) -> impl Iterator<Item = &syn::ItemEnum> {
185235
self.items.iter().filter_map(|i| match i {
186236
syn::Item::Enum(item) => Some(item),

tests/auction-house

tests/cashiers-check/Anchor.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ cashiers_check = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
77

88
[scripts]
99
test = "yarn run mocha -t 1000000 tests/"
10+
11+
[features]
12+
safety_checks = false

tests/cfo/Anchor.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ program = "./deps/stake/target/deploy/registry.so"
4141
[[test.genesis]]
4242
address = "6ebQNeTPZ1j7k3TtkCCtEPRvG7GQsucQrZ7sSEDQi9Ks"
4343
program = "./deps/stake/target/deploy/lockup.so"
44+
45+
[features]
46+
safety_checks = false

0 commit comments

Comments
 (0)