1pub mod concurrency;
4pub mod fixgen;
5pub mod frontends;
6
7use std::collections::HashMap;
8use std::path::Path;
9
10use padlock_core::arch::ArchConfig;
11use padlock_core::findings::SkippedStruct;
12use padlock_core::ir::{StructLayout, TypeInfo};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum CppStdlib {
20 #[default]
22 LibStdCpp,
23 LibCpp,
25 Msvc,
27}
28
29pub fn set_cpp_stdlib(stdlib: CppStdlib) {
35 frontends::c_cpp::set_stdlib(stdlib);
36}
37
38thread_local! {
46 static SKIPPED_COLLECTOR: std::cell::RefCell<Vec<SkippedStruct>> =
47 const { std::cell::RefCell::new(Vec::new()) };
48}
49
50pub fn record_skipped(name: &str, reason: &str) {
55 SKIPPED_COLLECTOR.with(|c| {
56 c.borrow_mut().push(SkippedStruct {
57 name: name.to_string(),
58 reason: reason.to_string(),
59 source_file: None,
60 });
61 });
62}
63
64fn take_skipped() -> Vec<SkippedStruct> {
65 SKIPPED_COLLECTOR.with(|c| c.take())
66}
67
68pub struct ParseOutput {
71 pub layouts: Vec<StructLayout>,
72 pub skipped: Vec<SkippedStruct>,
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub enum SourceLanguage {
77 C,
78 Cpp,
79 Rust,
80 Go,
81 Zig,
82}
83
84pub fn detect_language(path: &Path) -> Option<SourceLanguage> {
86 match path.extension().and_then(|e| e.to_str()) {
87 Some("c") | Some("h") => Some(SourceLanguage::C),
88 Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") => Some(SourceLanguage::Cpp),
89 Some("rs") => Some(SourceLanguage::Rust),
90 Some("go") => Some(SourceLanguage::Go),
91 Some("zig") => Some(SourceLanguage::Zig),
92 _ => None,
93 }
94}
95
96pub fn parse_source(path: &Path, arch: &'static ArchConfig) -> anyhow::Result<ParseOutput> {
98 let lang = detect_language(path)
99 .ok_or_else(|| anyhow::anyhow!("unsupported file type: {}", path.display()))?;
100 let source = std::fs::read_to_string(path)?;
101 let _ = take_skipped();
103 let mut layouts = parse_source_str(&source, &lang, arch)?;
104 let mut skipped = take_skipped();
105 let file_str = path.to_string_lossy().into_owned();
106 for layout in &mut layouts {
107 layout.source_file = Some(file_str.clone());
108 }
109 for s in &mut skipped {
110 s.source_file = Some(file_str.clone());
111 }
112 Ok(ParseOutput { layouts, skipped })
113}
114
115pub fn parse_source_str(
117 source: &str,
118 lang: &SourceLanguage,
119 arch: &'static ArchConfig,
120) -> anyhow::Result<Vec<StructLayout>> {
121 let mut layouts = match lang {
122 SourceLanguage::C => frontends::c_cpp::parse_c(source, arch)?,
123 SourceLanguage::Cpp => frontends::c_cpp::parse_cpp(source, arch)?,
124 SourceLanguage::Rust => frontends::rust::parse_rust(source, arch)?,
125 SourceLanguage::Go => frontends::go::parse_go(source, arch)?,
126 SourceLanguage::Zig => frontends::zig::parse_zig(source, arch)?,
127 };
128
129 resolve_nested_structs(&mut layouts);
132
133 for layout in &mut layouts {
135 concurrency::annotate_concurrency(layout, lang);
136 }
137
138 layouts.retain(|layout| !is_padlock_ignored(source, &layout.name));
140
141 Ok(layouts)
142}
143
144fn is_known_primitive(name: &str) -> bool {
149 matches!(
150 name,
151 "bool" | "u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "f32" | "u64" | "i64" | "f64"
153 | "u128" | "i128" | "usize" | "isize" | "char" | "str"
154 | "int" | "long" | "short" | "float" | "double" | "void"
156 | "int8_t" | "uint8_t" | "int16_t" | "uint16_t" | "int32_t" | "uint32_t"
157 | "int64_t" | "uint64_t" | "size_t" | "ssize_t" | "ptrdiff_t"
158 | "intptr_t" | "uintptr_t" | "_Bool"
159 | "int8" | "uint8" | "byte" | "int16" | "uint16" | "int32" | "uint32"
161 | "int64" | "uint64" | "float32" | "float64" | "complex64" | "complex128"
162 | "rune" | "string" | "error"
163 | "__m64" | "__m128" | "__m128d" | "__m128i"
165 | "__m256" | "__m256d" | "__m256i"
166 | "__m512" | "__m512d" | "__m512i"
167 )
168}
169
170fn resolve_nested_structs(layouts: &mut [StructLayout]) {
175 loop {
176 let known: HashMap<String, (usize, usize)> = layouts
178 .iter()
179 .map(|l| (l.name.clone(), (l.total_size, l.align)))
180 .collect();
181
182 let mut changed_any = false;
183
184 for layout in layouts.iter_mut() {
185 let mut changed = false;
186
187 for field in layout.fields.iter_mut() {
188 let type_name: String = match &field.ty {
191 TypeInfo::Primitive { name, .. } | TypeInfo::Opaque { name, .. } => {
192 name.clone()
193 }
194 _ => continue,
195 };
196
197 if is_known_primitive(&type_name) {
199 continue;
200 }
201
202 if type_name == layout.name {
204 continue;
205 }
206
207 if let Some(&(struct_size, struct_align)) = known.get(&type_name) {
208 if field.size == struct_size && field.align == struct_align {
211 continue;
212 }
213 let eff_align = if layout.is_packed { 1 } else { struct_align };
214 field.ty = TypeInfo::Opaque {
215 name: type_name,
216 size: struct_size,
217 align: struct_align,
218 };
219 field.size = struct_size;
220 field.align = eff_align;
221 changed = true;
222 }
223 }
224
225 if changed {
226 resimulate_layout(layout);
227 changed_any = true;
228 }
229 }
230
231 if !changed_any {
232 break;
233 }
234 }
235
236 let known_names: std::collections::HashSet<String> =
240 layouts.iter().map(|l| l.name.clone()).collect();
241
242 for layout in layouts.iter_mut() {
243 for field in layout.fields.iter() {
244 if let TypeInfo::Opaque {
245 name: type_name, ..
246 } = &field.ty
247 {
248 if is_known_primitive(type_name)
249 || type_name == &layout.name
250 || known_names.contains(type_name.as_str() as &str)
251 {
252 continue;
253 }
254 if !layout.uncertain_fields.contains(&field.name) {
255 layout.uncertain_fields.push(field.name.clone());
256 }
257 }
258 }
259 }
260}
261
262fn resimulate_layout(layout: &mut StructLayout) {
264 if layout.is_union {
265 for field in layout.fields.iter_mut() {
266 field.offset = 0;
267 }
268 let max_size = layout.fields.iter().map(|f| f.size).max().unwrap_or(0);
269 let max_align = layout.fields.iter().map(|f| f.align).max().unwrap_or(1);
270 layout.total_size = if max_align > 0 {
271 max_size.next_multiple_of(max_align)
272 } else {
273 max_size
274 };
275 layout.align = max_align;
276 return;
277 }
278
279 let packed = layout.is_packed;
280 let mut offset = 0usize;
281 let mut struct_align = 1usize;
282
283 for field in layout.fields.iter_mut() {
284 let eff_align = if packed { 1 } else { field.align };
285 if eff_align > 0 {
286 offset = offset.next_multiple_of(eff_align);
287 }
288 field.offset = offset;
289 offset += field.size;
290 struct_align = struct_align.max(eff_align);
291 }
292
293 if !packed && struct_align > 0 {
294 offset = offset.next_multiple_of(struct_align);
295 }
296
297 layout.total_size = offset;
298 layout.align = struct_align;
299}
300
301fn is_padlock_ignored(source: &str, struct_name: &str) -> bool {
311 for keyword in &["struct", "union", "type"] {
313 let needle = format!("{keyword} {struct_name}");
314 let mut search = 0usize;
315 while let Some(rel) = source[search..].find(&needle) {
316 let abs = search + rel;
317 let after_name = abs + needle.len();
319 let is_boundary = source[after_name..]
320 .chars()
321 .next()
322 .is_none_or(|c| !c.is_alphanumeric() && c != '_');
323 if is_boundary {
324 let line_start = source[..abs].rfind('\n').map(|i| i + 1).unwrap_or(0);
325 let line_end = source[abs..]
327 .find('\n')
328 .map(|i| abs + i)
329 .unwrap_or(source.len());
330 if source[line_start..line_end].contains("padlock:ignore") {
331 return true;
332 }
333 if line_start > 0 {
338 let prev_end = line_start - 1;
339 let prev_start = source[..prev_end].rfind('\n').map(|i| i + 1).unwrap_or(0);
340 let prev_trimmed = source[prev_start..prev_end].trim();
341 if prev_trimmed.starts_with("//") && prev_trimmed.contains("padlock:ignore") {
342 return true;
343 }
344 }
345 }
346 search = abs + 1;
347 }
348 }
349 false
350}
351
352#[cfg(test)]
355mod tests {
356 use super::*;
357 use padlock_core::arch::X86_64_SYSV;
358
359 #[test]
360 fn detect_c_extensions() {
361 assert_eq!(detect_language(Path::new("foo.c")), Some(SourceLanguage::C));
362 assert_eq!(detect_language(Path::new("foo.h")), Some(SourceLanguage::C));
363 }
364
365 #[test]
366 fn detect_cpp_extensions() {
367 assert_eq!(
368 detect_language(Path::new("foo.cpp")),
369 Some(SourceLanguage::Cpp)
370 );
371 assert_eq!(
372 detect_language(Path::new("foo.cc")),
373 Some(SourceLanguage::Cpp)
374 );
375 assert_eq!(
376 detect_language(Path::new("foo.hpp")),
377 Some(SourceLanguage::Cpp)
378 );
379 }
380
381 #[test]
382 fn detect_rust_extension() {
383 assert_eq!(
384 detect_language(Path::new("foo.rs")),
385 Some(SourceLanguage::Rust)
386 );
387 }
388
389 #[test]
390 fn detect_go_extension() {
391 assert_eq!(
392 detect_language(Path::new("foo.go")),
393 Some(SourceLanguage::Go)
394 );
395 }
396
397 #[test]
398 fn detect_zig_extension() {
399 assert_eq!(
400 detect_language(Path::new("foo.zig")),
401 Some(SourceLanguage::Zig)
402 );
403 }
404
405 #[test]
406 fn detect_unknown_is_none() {
407 assert_eq!(detect_language(Path::new("foo.py")), None);
408 assert_eq!(detect_language(Path::new("foo")), None);
409 }
410
411 #[test]
412 fn parse_source_str_c_roundtrip() {
413 let src = "struct Point { int x; int y; };";
414 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
415 assert_eq!(layouts.len(), 1);
416 assert_eq!(layouts[0].name, "Point");
417 }
418
419 #[test]
420 fn parse_source_str_rust_roundtrip() {
421 let src = "struct Foo { x: u32, y: u64 }";
422 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
423 assert_eq!(layouts.len(), 1);
424 assert_eq!(layouts[0].name, "Foo");
425 }
426
427 #[test]
428 fn padlock_ignore_suppresses_c_struct() {
429 let src = "// padlock:ignore\nstruct Hidden { int x; int y; };\nstruct Visible { int a; };";
430 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
431 assert_eq!(layouts.len(), 1);
432 assert_eq!(layouts[0].name, "Visible");
433 }
434
435 #[test]
436 fn padlock_ignore_inline_suppresses_c_struct() {
437 let src = "struct Hidden { int x; }; // padlock:ignore\nstruct Visible { int a; };";
441 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
442 assert_eq!(layouts.len(), 1, "only Visible should remain");
443 assert_eq!(layouts[0].name, "Visible");
444 }
445
446 #[test]
447 fn padlock_ignore_suppresses_rust_struct() {
448 let src = "// padlock:ignore\nstruct Hidden { x: u32 }\nstruct Visible { a: u32 }";
449 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
450 assert_eq!(layouts.len(), 1);
451 assert_eq!(layouts[0].name, "Visible");
452 }
453
454 #[test]
455 fn padlock_ignore_without_annotation_keeps_struct() {
456 let src = "struct Visible { int x; int y; };";
457 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
458 assert_eq!(layouts.len(), 1);
459 assert_eq!(layouts[0].name, "Visible");
460 }
461
462 #[test]
465 fn nested_rust_struct_size_resolved() {
466 let src = "struct Inner { x: u64 }\nstruct Outer { a: u8, b: Inner }";
470 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
471 let outer = layouts.iter().find(|l| l.name == "Outer").unwrap();
472 let b = outer.fields.iter().find(|f| f.name == "b").unwrap();
473 assert_eq!(b.size, 8, "Inner is 8 bytes");
474 assert_eq!(b.align, 8, "Inner aligns to 8");
475 assert_eq!(outer.total_size, 16);
477 }
478
479 #[test]
480 fn nested_rust_struct_non_pointer_size_resolved() {
481 let src = "struct Point { x: i32, y: i32 }\nstruct Line { a: Point, b: Point }";
484 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
485 let line = layouts.iter().find(|l| l.name == "Line").unwrap();
486 assert_eq!(line.total_size, 16);
487 assert_eq!(line.fields[0].size, 8);
488 assert_eq!(line.fields[1].size, 8);
489 assert_eq!(line.fields[1].offset, 8);
490 }
491
492 #[test]
493 fn nested_rust_struct_large_inner_triggers_padding() {
494 let src = "struct SmallHeader { flag: bool }\nstruct Wrapper { h: SmallHeader, data: u64 }";
500 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
501 let wrapper = layouts.iter().find(|l| l.name == "Wrapper").unwrap();
502 let h = wrapper.fields.iter().find(|f| f.name == "h").unwrap();
503 assert_eq!(h.size, 1, "SmallHeader resolved to 1 byte");
505 assert_eq!(h.align, 1);
506 let data = wrapper.fields.iter().find(|f| f.name == "data").unwrap();
508 assert_eq!(data.offset, 8);
509 assert_eq!(wrapper.total_size, 16);
510 }
511
512 #[test]
513 fn nested_c_struct_resolved() {
514 let src =
515 "struct Vec2 { float x; float y; };\nstruct Rect { struct Vec2 tl; struct Vec2 br; };";
516 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
517 let rect = layouts.iter().find(|l| l.name == "Rect").unwrap();
518 assert_eq!(rect.total_size, 16, "Rect should be 16 bytes");
520 assert_eq!(rect.fields[0].size, 8);
521 assert_eq!(rect.fields[1].size, 8);
522 assert_eq!(rect.fields[1].offset, 8);
523 }
524
525 #[test]
526 fn nested_go_struct_resolved() {
527 let src = "package p\ntype Vec2 struct { X float32; Y float32 }\ntype Rect struct { TL Vec2; BR Vec2 }";
528 let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
529 let rect = layouts.iter().find(|l| l.name == "Rect").unwrap();
530 assert_eq!(rect.total_size, 16);
531 assert_eq!(rect.fields[0].size, 8);
532 assert_eq!(rect.fields[1].size, 8);
533 assert_eq!(rect.fields[1].offset, 8);
534 }
535
536 #[test]
537 fn primitive_types_not_shadowed_by_struct_resolution() {
538 let src = "struct Wrapper { x: u64, y: bool }";
540 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
541 let w = &layouts[0];
542 let x = w.fields.iter().find(|f| f.name == "x").unwrap();
543 assert_eq!(x.size, 8, "u64 must stay 8 bytes");
544 }
545
546 #[test]
547 fn is_padlock_ignored_does_not_match_partial_names() {
548 assert!(!is_padlock_ignored(
550 "// padlock:ignore\nstruct FooBar { int x; };",
551 "Foo"
552 ));
553 }
554
555 #[test]
558 fn per_finding_suppress_reorder_in_c() {
559 let src = "// padlock: ignore[ReorderSuggestion]\nstruct Foo { char a; long b; };";
563 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
564 assert_eq!(layouts.len(), 1);
565 assert_eq!(layouts[0].suppressed_findings, vec!["ReorderSuggestion"]);
566 }
567
568 #[test]
569 fn per_finding_suppress_multiple_kinds_in_c() {
570 let src =
571 "// padlock: ignore[PaddingWaste, ReorderSuggestion]\nstruct Bar { char a; long b; };";
572 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
573 assert_eq!(layouts.len(), 1);
574 assert_eq!(
575 layouts[0].suppressed_findings,
576 vec!["PaddingWaste", "ReorderSuggestion"]
577 );
578 }
579
580 #[test]
581 fn per_finding_suppress_in_rust() {
582 let src = "// padlock: ignore[FalseSharing]\nstruct Foo { x: u64, y: u64 }";
583 let layouts = parse_source_str(src, &SourceLanguage::Rust, &X86_64_SYSV).unwrap();
584 assert_eq!(layouts.len(), 1);
585 assert_eq!(layouts[0].suppressed_findings, vec!["FalseSharing"]);
586 }
587
588 #[test]
589 fn per_finding_suppress_in_go() {
590 let src =
591 "package p\n// padlock: ignore[LocalityIssue]\ntype Foo struct { X int64; Y int64 }";
592 let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
593 assert_eq!(layouts.len(), 1);
594 assert_eq!(layouts[0].suppressed_findings, vec!["LocalityIssue"]);
595 }
596
597 #[test]
598 fn unannotated_struct_has_no_suppressed_findings() {
599 let src = "struct Clean { int x; int y; };";
600 let layouts = parse_source_str(src, &SourceLanguage::C, &X86_64_SYSV).unwrap();
601 assert_eq!(layouts.len(), 1);
602 assert!(layouts[0].suppressed_findings.is_empty());
603 }
604
605 #[test]
608 fn cpp_inheritance_base_size_resolved_via_parse_source_str() {
609 let src = r#"
613class SmallBase { int x; };
614class BigDerived : public SmallBase { int a; int b; int c; };
615"#;
616 let layouts = parse_source_str(src, &SourceLanguage::Cpp, &X86_64_SYSV).unwrap();
617 let derived = layouts.iter().find(|l| l.name == "BigDerived").unwrap();
618 let base_field = derived
619 .fields
620 .iter()
621 .find(|f| f.name == "__base_SmallBase")
622 .unwrap();
623 assert_eq!(
626 base_field.size, 4,
627 "__base_SmallBase should be resolved to 4 bytes (sizeof SmallBase)"
628 );
629 assert_eq!(derived.total_size, 16);
631 }
632
633 #[test]
634 fn cpp_multi_level_inheritance_resolved() {
635 let src = r#"
636class A { int x; };
637class B : public A { int y; };
638class C : public B { int z; };
639"#;
640 let layouts = parse_source_str(src, &SourceLanguage::Cpp, &X86_64_SYSV).unwrap();
641 let c = layouts.iter().find(|l| l.name == "C").unwrap();
642 let base_b = c.fields.iter().find(|f| f.name == "__base_B").unwrap();
644 assert_eq!(base_b.size, 8, "B is 8 bytes (A's int + B's int)");
645 assert_eq!(c.total_size, 12);
646 }
647}