1use padlock_core::arch::ArchConfig;
7use padlock_core::ir::{AccessPattern, Field, StructLayout, TypeInfo};
8use std::collections::HashSet;
9use tree_sitter::{Node, Parser};
10
11fn go_type_size_align(ty: &str, arch: &'static ArchConfig) -> (usize, usize) {
14 match ty.trim() {
15 "bool" => (1, 1),
16 "int8" | "uint8" | "byte" => (1, 1),
17 "int16" | "uint16" => (2, 2),
18 "int32" | "uint32" | "rune" | "float32" => (4, 4),
19 "int64" | "uint64" | "float64" | "complex64" => (8, 8),
20 "complex128" => (16, 16),
21 "int" | "uint" => (arch.pointer_size, arch.pointer_size),
22 "uintptr" => (arch.pointer_size, arch.pointer_size),
23 "string" => (arch.pointer_size * 2, arch.pointer_size), ty if ty.starts_with("[]") => (arch.pointer_size * 3, arch.pointer_size), ty if ty.starts_with("map[") || ty.starts_with("chan ") => {
26 (arch.pointer_size, arch.pointer_size)
27 }
28 ty if ty.starts_with('*') => (arch.pointer_size, arch.pointer_size),
29 "error" | "any" => (arch.pointer_size * 2, arch.pointer_size),
42 ty if ty.starts_with("interface") => (arch.pointer_size * 2, arch.pointer_size),
43 _ => (arch.pointer_size, arch.pointer_size),
44 }
45}
46
47fn collect_go_interface_names(source: &str, root: Node<'_>) -> HashSet<String> {
56 let mut names = HashSet::new();
57 let mut stack = vec![root];
58 while let Some(node) = stack.pop() {
59 for i in (0..node.child_count()).rev() {
60 if let Some(child) = node.child(i) {
61 stack.push(child);
62 }
63 }
64 if node.kind() != "type_spec" {
65 continue;
66 }
67 let mut iface_name: Option<String> = None;
69 let mut is_interface = false;
70 for i in 0..node.child_count() {
71 let Some(child) = node.child(i) else { continue };
72 match child.kind() {
73 "type_identifier" => {
74 iface_name = Some(source[child.byte_range()].to_string());
75 }
76 "interface_type" => {
77 is_interface = true;
78 }
79 _ => {}
80 }
81 }
82 if is_interface && let Some(name) = iface_name {
83 names.insert(name);
84 }
85 }
86 names
87}
88
89fn extract_structs(source: &str, root: Node<'_>, arch: &'static ArchConfig) -> Vec<StructLayout> {
92 let local_interfaces = collect_go_interface_names(source, root);
94
95 let mut layouts = Vec::new();
96 let mut stack = vec![root];
97
98 while let Some(node) = stack.pop() {
99 for i in (0..node.child_count()).rev() {
100 if let Some(c) = node.child(i) {
101 stack.push(c);
102 }
103 }
104
105 if node.kind() == "type_declaration"
107 && let Some(layout) = parse_type_declaration(source, node, arch, &local_interfaces)
108 {
109 layouts.push(layout);
110 }
111 }
112 layouts
113}
114
115fn parse_type_declaration(
116 source: &str,
117 node: Node<'_>,
118 arch: &'static ArchConfig,
119 local_interfaces: &HashSet<String>,
120) -> Option<StructLayout> {
121 let source_line = node.start_position().row as u32 + 1;
122 let decl_start_byte = node.start_byte();
123 for i in 0..node.child_count() {
125 let child = node.child(i)?;
126 if child.kind() == "type_spec" {
127 return parse_type_spec(
128 source,
129 child,
130 arch,
131 source_line,
132 decl_start_byte,
133 local_interfaces,
134 );
135 }
136 }
137 None
138}
139
140fn parse_type_spec(
141 source: &str,
142 node: Node<'_>,
143 arch: &'static ArchConfig,
144 source_line: u32,
145 decl_start_byte: usize,
146 local_interfaces: &HashSet<String>,
147) -> Option<StructLayout> {
148 let mut name: Option<String> = None;
149 let mut struct_node: Option<Node> = None;
150
151 for i in 0..node.child_count() {
152 let child = node.child(i)?;
153 match child.kind() {
154 "type_identifier" => name = Some(source[child.byte_range()].to_string()),
155 "struct_type" => struct_node = Some(child),
156 _ => {}
157 }
158 }
159
160 let name = name?;
161 let struct_node = struct_node?;
162 parse_struct_type(
163 source,
164 struct_node,
165 name,
166 arch,
167 source_line,
168 decl_start_byte,
169 local_interfaces,
170 )
171}
172
173fn parse_struct_type(
174 source: &str,
175 node: Node<'_>,
176 name: String,
177 arch: &'static ArchConfig,
178 source_line: u32,
179 decl_start_byte: usize,
180 local_interfaces: &HashSet<String>,
181) -> Option<StructLayout> {
182 let mut raw_fields: Vec<(String, String, Option<String>, u32)> = Vec::new();
183
184 for i in 0..node.child_count() {
185 let child = node.child(i)?;
186 if child.kind() == "field_declaration_list" {
187 for j in 0..child.child_count() {
188 let field_node = child.child(j)?;
189 if field_node.kind() == "field_declaration" {
190 collect_field_declarations(source, field_node, &mut raw_fields);
191 }
192 }
193 }
194 }
195
196 if raw_fields.is_empty() {
197 return None;
198 }
199
200 let mut offset = 0usize;
202 let mut struct_align = 1usize;
203 let mut fields: Vec<Field> = Vec::new();
204 let mut uncertain_fields: Vec<String> = Vec::new();
205
206 for (fname, ty_name, guard, field_line) in raw_fields {
207 let (mut size, mut align) = go_type_size_align(&ty_name, arch);
208
209 if local_interfaces.contains(ty_name.as_str()) {
212 size = arch.pointer_size * 2;
213 align = arch.pointer_size;
214 }
215
216 let is_pointer = ty_name.starts_with('*');
221 let base_ty = ty_name.trim_start_matches('*');
222 if !is_pointer && base_ty.contains('.') {
223 uncertain_fields.push(fname.clone());
224 }
225
226 if align > 0 {
227 offset = offset.next_multiple_of(align);
228 }
229 struct_align = struct_align.max(align);
230 let access = if let Some(g) = guard {
231 AccessPattern::Concurrent {
232 guard: Some(g),
233 is_atomic: false,
234 is_annotated: true,
235 }
236 } else {
237 AccessPattern::Unknown
238 };
239 fields.push(Field {
240 name: fname,
241 ty: TypeInfo::Primitive {
242 name: ty_name,
243 size,
244 align,
245 },
246 offset,
247 size,
248 align,
249 source_file: None,
250 source_line: Some(field_line),
251 access,
252 });
253 offset += size;
254 }
255 if struct_align > 0 {
256 offset = offset.next_multiple_of(struct_align);
257 }
258
259 Some(StructLayout {
260 name,
261 total_size: offset,
262 align: struct_align,
263 fields,
264 source_file: None,
265 source_line: Some(source_line),
266 arch,
267 is_packed: false,
268 is_union: false,
269 is_repr_rust: false,
270 suppressed_findings: super::suppress::suppressed_from_preceding_source(
271 source,
272 decl_start_byte,
273 ),
274 uncertain_fields,
275 })
276}
277
278pub fn extract_guard_from_go_comment(comment: &str) -> Option<String> {
285 let c = comment.trim();
286 let body = c.strip_prefix("//").map(str::trim)?;
288
289 if let Some(rest) = body.strip_prefix("padlock:guard=") {
291 let guard = rest.trim();
292 if !guard.is_empty() {
293 return Some(guard.to_string());
294 }
295 }
296 if let Some(rest) = body
298 .strip_prefix("guarded_by:")
299 .or_else(|| body.strip_prefix("guarded_by ="))
300 {
301 let guard = rest.trim();
302 if !guard.is_empty() {
303 return Some(guard.to_string());
304 }
305 }
306 if let Some(rest) = body.strip_prefix("+checklocksprotects:") {
308 let guard = rest.trim();
309 if !guard.is_empty() {
310 return Some(guard.to_string());
311 }
312 }
313 None
314}
315
316fn trailing_comment_on_line(source: &str, node: Node<'_>) -> Option<String> {
318 let end = node.end_byte();
321 if end >= source.len() {
322 return None;
323 }
324 let rest = &source[end..];
325 let line = rest.lines().next().unwrap_or("");
327 line.find("//").map(|pos| line[pos..].to_string())
329}
330
331fn collect_field_declarations(
332 source: &str,
333 node: Node<'_>,
334 out: &mut Vec<(String, String, Option<String>, u32)>,
335) {
336 let mut field_names: Vec<String> = Vec::new();
339 let mut ty_text: Option<String> = None;
340 let field_line = node.start_position().row as u32 + 1;
341
342 for i in 0..node.child_count() {
343 if let Some(child) = node.child(i) {
344 match child.kind() {
345 "field_identifier" => field_names.push(source[child.byte_range()].to_string()),
346 "type_identifier" | "pointer_type" | "qualified_type" | "slice_type"
347 | "map_type" | "channel_type" | "array_type" | "interface_type" => {
348 ty_text = Some(source[child.byte_range()].trim().to_string());
349 }
350 _ => {}
351 }
352 }
353 }
354
355 let guard =
356 trailing_comment_on_line(source, node).and_then(|c| extract_guard_from_go_comment(&c));
357
358 if !field_names.is_empty() {
359 if let Some(ty) = ty_text {
360 for name in field_names {
362 out.push((name, ty.clone(), guard.clone(), field_line));
363 }
364 }
365 } else if let Some(ty) = ty_text {
366 let simple_name = ty.split('.').next_back().unwrap_or(&ty).to_string();
371 out.push((simple_name, ty, guard, field_line));
372 }
373}
374
375pub fn parse_go(source: &str, arch: &'static ArchConfig) -> anyhow::Result<Vec<StructLayout>> {
378 let mut parser = Parser::new();
379 parser.set_language(&tree_sitter_go::LANGUAGE.into())?;
380 let tree = parser
381 .parse(source, None)
382 .ok_or_else(|| anyhow::anyhow!("tree-sitter-go parse failed"))?;
383 Ok(extract_structs(source, tree.root_node(), arch))
384}
385
386#[cfg(test)]
389mod tests {
390 use super::*;
391 use padlock_core::arch::X86_64_SYSV;
392
393 #[test]
394 fn parse_simple_go_struct() {
395 let src = r#"
396package main
397type Point struct {
398 X int32
399 Y int32
400}
401"#;
402 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
403 assert_eq!(layouts.len(), 1);
404 assert_eq!(layouts[0].name, "Point");
405 assert_eq!(layouts[0].fields.len(), 2);
406 }
407
408 #[test]
409 fn go_layout_with_padding() {
410 let src = "package p\ntype T struct { A bool; B int64 }";
411 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
412 assert_eq!(layouts.len(), 1);
413 let l = &layouts[0];
414 assert_eq!(l.fields[0].offset, 0);
415 assert_eq!(l.fields[1].offset, 8); }
417
418 #[test]
419 fn go_string_is_two_words() {
420 let src = "package p\ntype S struct { Name string }";
421 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
422 assert_eq!(layouts[0].fields[0].size, 16); }
424
425 #[test]
428 fn extract_guard_padlock_form() {
429 assert_eq!(
430 extract_guard_from_go_comment("// padlock:guard=mu"),
431 Some("mu".to_string())
432 );
433 }
434
435 #[test]
436 fn extract_guard_guarded_by_form() {
437 assert_eq!(
438 extract_guard_from_go_comment("// guarded_by: counter_lock"),
439 Some("counter_lock".to_string())
440 );
441 }
442
443 #[test]
444 fn extract_guard_checklocksprotects_form() {
445 assert_eq!(
446 extract_guard_from_go_comment("// +checklocksprotects:mu"),
447 Some("mu".to_string())
448 );
449 }
450
451 #[test]
452 fn extract_guard_no_match_returns_none() {
453 assert!(extract_guard_from_go_comment("// just a comment").is_none());
454 assert!(extract_guard_from_go_comment("// TODO: fix this").is_none());
455 }
456
457 #[test]
458 fn go_struct_padlock_guard_annotation_sets_concurrent() {
459 let src = r#"package p
460type Cache struct {
461 Readers int64 // padlock:guard=mu
462 Writers int64 // padlock:guard=other_mu
463 Mu sync.Mutex
464}
465"#;
466 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
467 let l = &layouts[0];
468 if let AccessPattern::Concurrent { guard, .. } = &l.fields[0].access {
470 assert_eq!(guard.as_deref(), Some("mu"));
471 } else {
472 panic!(
473 "expected Concurrent for Readers, got {:?}",
474 l.fields[0].access
475 );
476 }
477 if let AccessPattern::Concurrent { guard, .. } = &l.fields[1].access {
478 assert_eq!(guard.as_deref(), Some("other_mu"));
479 } else {
480 panic!(
481 "expected Concurrent for Writers, got {:?}",
482 l.fields[1].access
483 );
484 }
485 }
486
487 #[test]
488 fn go_struct_different_guards_same_cache_line_is_false_sharing() {
489 let src = r#"package p
490type HotPath struct {
491 Readers int64 // padlock:guard=lock_a
492 Writers int64 // padlock:guard=lock_b
493}
494"#;
495 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
496 assert!(padlock_core::analysis::false_sharing::has_false_sharing(
497 &layouts[0]
498 ));
499 }
500
501 #[test]
502 fn go_struct_same_guard_is_not_false_sharing() {
503 let src = r#"package p
504type Safe struct {
505 A int64 // padlock:guard=mu
506 B int64 // padlock:guard=mu
507}
508"#;
509 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
510 assert!(!padlock_core::analysis::false_sharing::has_false_sharing(
511 &layouts[0]
512 ));
513 }
514
515 #[test]
518 fn interface_field_is_two_words() {
519 let src = "package p\ntype S struct { V interface{} }";
521 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
522 assert_eq!(layouts[0].fields[0].size, 16); assert_eq!(layouts[0].fields[0].align, 8);
524 }
525
526 #[test]
527 fn any_field_is_two_words() {
528 let src = "package p\ntype S struct { V any }";
530 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
531 assert_eq!(layouts[0].fields[0].size, 16); assert_eq!(layouts[0].fields[0].align, 8);
533 }
534
535 #[test]
536 fn interface_field_same_size_as_error() {
537 let src_iface = "package p\ntype S struct { V interface{} }";
539 let src_err = "package p\ntype S struct { V error }";
540 let iface = parse_go(src_iface, &X86_64_SYSV).unwrap();
541 let err = parse_go(src_err, &X86_64_SYSV).unwrap();
542 assert_eq!(iface[0].fields[0].size, err[0].fields[0].size);
543 }
544
545 #[test]
546 fn struct_with_mixed_interface_and_ints_has_correct_layout() {
547 let src = "package p\ntype S struct { V interface{}; N int64 }";
549 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
550 let l = &layouts[0];
551 assert_eq!(l.fields[0].offset, 0);
552 assert_eq!(l.fields[0].size, 16);
553 assert_eq!(l.fields[1].offset, 16);
554 assert_eq!(l.total_size, 24);
555 }
556
557 #[test]
558 fn inline_interface_with_methods_is_two_words() {
559 let src = "package p\ntype S struct { Conn interface{ Close() error } }";
564 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
565 assert_eq!(layouts[0].fields[0].size, 16);
566 assert_eq!(layouts[0].fields[0].align, 8);
567 }
568
569 #[test]
570 fn named_cross_package_interface_falls_back_to_pointer_size() {
571 let src = "package p\ntype DB struct { connector driver.Connector }";
578 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
579 assert_eq!(
581 layouts[0].fields[0].size, 8,
582 "named cross-package interface falls back to pointer_size (known limitation)"
583 );
584 assert!(
586 layouts[0]
587 .uncertain_fields
588 .contains(&"connector".to_string()),
589 "qualified-type field should be in uncertain_fields"
590 );
591 }
592
593 #[test]
596 fn local_interface_field_is_fat_pointer() {
597 let src = r#"package p
600type Reader interface {
601 Read(p []byte) (n int, err error)
602}
603type Buf struct {
604 R Reader
605 N int32
606}
607"#;
608 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
609 let l = layouts.iter().find(|l| l.name == "Buf").expect("Buf");
610 let r = l.fields.iter().find(|f| f.name == "R").expect("R field");
611 assert_eq!(
612 r.size, 16,
613 "local interface must be sized as 16B fat pointer"
614 );
615 assert_eq!(r.align, 8);
616 }
617
618 #[test]
619 fn local_interface_field_not_marked_uncertain() {
620 let src = r#"package p
622type Closer interface { Close() error }
623type File struct { C Closer }
624"#;
625 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
626 let l = layouts.iter().find(|l| l.name == "File").expect("File");
627 assert!(
628 !l.uncertain_fields.contains(&"C".to_string()),
629 "local interface field must not be uncertain"
630 );
631 }
632
633 #[test]
634 fn qualified_type_field_marked_uncertain() {
635 let src = "package p\ntype S struct { R io.Reader; N int32 }";
638 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
639 let l = &layouts[0];
640 assert!(
641 l.uncertain_fields.contains(&"R".to_string()),
642 "qualified-type field must be in uncertain_fields"
643 );
644 assert!(
646 !l.uncertain_fields.contains(&"N".to_string()),
647 "plain int32 field must not be uncertain"
648 );
649 }
650
651 #[test]
652 fn pointer_to_qualified_type_not_uncertain() {
653 let src = "package p\ntype S struct { P *io.Reader }";
657 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
658 let l = &layouts[0];
659 assert!(
660 !l.uncertain_fields.contains(&"P".to_string()),
661 "*qualified.Type pointer must not be uncertain"
662 );
663 }
664
665 #[test]
668 fn embedded_struct_field_uses_type_name_as_field_name() {
669 let src = r#"package p
671type Base struct { X int32 }
672type Derived struct {
673 Base
674 Y int32
675}
676"#;
677 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
678 let derived = layouts
679 .iter()
680 .find(|l| l.name == "Derived")
681 .expect("Derived");
682 assert!(
684 derived.fields.iter().any(|f| f.name == "Base"),
685 "embedded field should be named 'Base'"
686 );
687 }
688
689 #[test]
690 fn embedded_qualified_type_uses_unqualified_name() {
691 let src = r#"package p
693type Safe struct {
694 sync.Mutex
695 Value int64
696}
697"#;
698 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
699 let l = layouts.iter().find(|l| l.name == "Safe").expect("Safe");
700 assert!(
701 l.fields.iter().any(|f| f.name == "Mutex"),
702 "embedded sync.Mutex should produce field named 'Mutex'"
703 );
704 }
705
706 #[test]
707 fn embedded_field_has_non_zero_size_from_resolution() {
708 let src = r#"package p
711type Inner struct { A int64; B int64 }
712type Outer struct {
713 Inner
714 C int32
715}
716"#;
717 use crate::{SourceLanguage, parse_source_str};
718 let layouts = parse_source_str(src, &SourceLanguage::Go, &X86_64_SYSV).unwrap();
719 let outer = layouts.iter().find(|l| l.name == "Outer").expect("Outer");
720 let inner_field = outer
721 .fields
722 .iter()
723 .find(|f| f.name == "Inner")
724 .expect("Inner field");
725 assert_eq!(
727 inner_field.size, 16,
728 "embedded Inner field should be resolved to 16 bytes"
729 );
730 }
731
732 #[test]
733 fn struct_with_no_embedded_fields_unaffected() {
734 let src = "package p\ntype S struct { A int32; B int64 }";
735 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
736 let l = &layouts[0];
737 assert_eq!(l.fields.len(), 2);
738 assert_eq!(l.fields[0].name, "A");
739 assert_eq!(l.fields[1].name, "B");
740 }
741
742 #[test]
745 fn embedded_unknown_type_falls_back_to_pointer_size() {
746 let src = "package p\ntype S struct { external.Type\nX int32 }";
748 let layouts = parse_go(src, &X86_64_SYSV).unwrap();
749 let l = layouts.iter().find(|l| l.name == "S").expect("S");
750 let emb = l
751 .fields
752 .iter()
753 .find(|f| f.name == "Type")
754 .expect("Type field");
755 assert_eq!(emb.size, 8);
757 }
758}