Skip to main content

padlock_core/
ir.rs

1//padlock-core/src/ir.rs
2
3pub use crate::arch::{ArchConfig, X86_64_SYSV};
4
5/// Serde helpers for serializing/deserializing `&'static ArchConfig` by name.
6mod arch_serde {
7    use crate::arch::{ArchConfig, arch_by_name};
8    use serde::{Deserialize, Deserializer, Serializer};
9
10    pub fn serialize<S: Serializer>(arch: &&'static ArchConfig, s: S) -> Result<S::Ok, S::Error> {
11        s.serialize_str(arch.name)
12    }
13
14    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<&'static ArchConfig, D::Error> {
15        let name = String::deserialize(d)?;
16        arch_by_name(&name).ok_or_else(|| {
17            serde::de::Error::custom(format!(
18                "unknown arch {name:?} in cache; \
19                 clear it with `rm -rf .padlock-cache`"
20            ))
21        })
22    }
23}
24
25/// The type of a single field. Recursive for nested structs.
26#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub enum TypeInfo {
28    Primitive {
29        name: String,
30        size: usize,
31        align: usize,
32    },
33    Pointer {
34        size: usize,
35        align: usize,
36    },
37    Array {
38        element: Box<TypeInfo>,
39        count: usize,
40        size: usize,
41        align: usize,
42    },
43    Struct(Box<StructLayout>),
44    Opaque {
45        name: String,
46        size: usize,
47        align: usize,
48    },
49}
50
51impl TypeInfo {
52    pub fn size(&self) -> usize {
53        match self {
54            TypeInfo::Primitive { size, .. } => *size,
55            TypeInfo::Pointer { size, .. } => *size,
56            TypeInfo::Array { size, .. } => *size,
57            TypeInfo::Struct(l) => l.total_size,
58            TypeInfo::Opaque { size, .. } => *size,
59        }
60    }
61
62    pub fn align(&self) -> usize {
63        match self {
64            TypeInfo::Primitive { align, .. } => *align,
65            TypeInfo::Pointer { align, .. } => *align,
66            TypeInfo::Array { align, .. } => *align,
67            TypeInfo::Struct(l) => l.align,
68            TypeInfo::Opaque { align, .. } => *align,
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
74pub enum AccessPattern {
75    Unknown,
76    Concurrent {
77        guard: Option<String>,
78        is_atomic: bool,
79        /// True when this pattern was set by an explicit source annotation
80        /// (e.g. `GUARDED_BY`, `#[lock_protected_by]`, `// padlock:guard=`).
81        /// False when inferred from the field's type name by the heuristic pass.
82        #[serde(default)]
83        is_annotated: bool,
84    },
85    ReadMostly,
86    Padding,
87}
88
89#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct Field {
91    pub name: String,
92    pub ty: TypeInfo,
93    pub offset: usize,
94    pub size: usize,
95    pub align: usize,
96    pub source_file: Option<String>,
97    pub source_line: Option<u32>,
98    pub access: AccessPattern,
99}
100
101/// One complete struct as read from DWARF or source and enriched by analysis.
102#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
103pub struct StructLayout {
104    pub name: String,
105    pub total_size: usize,
106    pub align: usize,
107    pub fields: Vec<Field>,
108    pub source_file: Option<String>,
109    pub source_line: Option<u32>,
110    #[serde(with = "arch_serde")]
111    pub arch: &'static ArchConfig,
112    pub is_packed: bool,
113    /// True when this layout was parsed from a C/C++ `union` declaration.
114    /// All fields share the same base offset (0); analysis suppresses reorder
115    /// and padding findings that do not apply to unions.
116    pub is_union: bool,
117    /// True when this is a Rust struct with `repr(Rust)` (i.e. no `#[repr(C)]`,
118    /// `#[repr(packed)]`, or `#[repr(transparent)]`). The compiler is free to
119    /// reorder fields and eliminate padding — padlock's findings describe
120    /// *declared-order* waste, which may not match the actual runtime layout.
121    /// Always `false` for DWARF/binary layouts (which are always accurate).
122    #[serde(default)]
123    pub is_repr_rust: bool,
124    /// Finding kinds that are suppressed for this struct via a source annotation.
125    ///
126    /// Populated by source frontends when they encounter a suppression directive:
127    /// - Rust: `#[padlock_suppress = "ReorderSuggestion,FalseSharing"]`
128    /// - C/C++/Go/Zig: `// padlock: ignore[ReorderSuggestion,FalseSharing]`
129    ///
130    /// Values match the variant names of [`padlock_core::findings::Finding`]:
131    /// `"PaddingWaste"`, `"ReorderSuggestion"`, `"FalseSharing"`, `"LocalityIssue"`.
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub suppressed_findings: Vec<String>,
134
135    /// Field names whose type size could not be accurately determined from source
136    /// alone (e.g. a qualified name like `driver.Connector` whose package is not
137    /// in the analyzed source set and may be an interface rather than a struct).
138    ///
139    /// When non-empty, padding and reorder findings on this struct may be
140    /// inaccurate. For precise sizing use binary analysis or `--go-types`.
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub uncertain_fields: Vec<String>,
143}
144
145#[derive(Debug, Clone, PartialEq, serde::Serialize)]
146pub struct PaddingGap {
147    pub after_field: String,
148    pub bytes: usize,
149    pub at_offset: usize,
150}
151
152#[derive(Debug, Clone, serde::Serialize)]
153pub struct SharingConflict {
154    pub fields: Vec<String>,
155    pub cache_line: usize,
156}
157
158/// Find all padding gaps between consecutive fields.
159///
160/// Returns an empty vec for union layouts — all fields share offset 0 by
161/// definition, so the concept of inter-field padding does not apply.
162pub fn find_padding(layout: &StructLayout) -> Vec<PaddingGap> {
163    if layout.is_union {
164        return Vec::new();
165    }
166    let mut gaps = Vec::new();
167    for window in layout.fields.windows(2) {
168        let current = &window[0];
169        let next = &window[1];
170        let end = current.offset + current.size;
171        if next.offset > end {
172            gaps.push(PaddingGap {
173                after_field: current.name.clone(),
174                bytes: next.offset - end,
175                at_offset: end,
176            });
177        }
178    }
179    // Trailing padding: struct total_size > last field end
180    if let Some(last) = layout.fields.last() {
181        let end = last.offset + last.size;
182        if layout.total_size > end {
183            gaps.push(PaddingGap {
184                after_field: last.name.clone(),
185                bytes: layout.total_size - end,
186                at_offset: end,
187            });
188        }
189    }
190    gaps
191}
192
193/// Return fields sorted by descending alignment then descending size (optimal order).
194///
195/// This minimises struct padding (same goal as go/analysis/passes/fieldalignment).
196///
197/// **GC note (Go-specific)**: fieldalignment's `optimalOrder` adds a secondary
198/// tie-breaking rule: when alignment is equal, pointer-bearing fields come before
199/// pointer-free fields to minimise the GC scan range (`ptrdata`).  padlock's IR
200/// stores all field types as language-agnostic `TypeInfo`, so pointer-vs-non-pointer
201/// cannot be determined here without language-specific knowledge.  For Go structs the
202/// resulting reorder order may differ from fieldalignment's suggestion by the GC-aware
203/// tie-break, but the padding savings are identical.  A future Go-specific sort path
204/// could incorporate this by carrying `has_gc_pointer: bool` on each `Field`.
205pub fn optimal_order(layout: &StructLayout) -> Vec<&Field> {
206    let mut sorted: Vec<&Field> = layout.fields.iter().collect();
207    sorted.sort_by(|a, b| {
208        b.align
209            .cmp(&a.align)
210            .then(b.size.cmp(&a.size))
211            .then(a.name.cmp(&b.name))
212    });
213    sorted
214}
215
216// ── tests ─────────────────────────────────────────────────────────────────────
217
218#[cfg(any(test, feature = "test-helpers"))]
219pub mod test_fixtures {
220    use super::*;
221    use crate::arch::X86_64_SYSV;
222
223    /// The canonical misaligned layout used across crate tests.
224    ///   is_active: bool  offset 0,  size 1, align 1
225    ///   [7 bytes padding]
226    ///   timeout:   f64   offset 8,  size 8, align 8
227    ///   is_tls:    bool  offset 16, size 1, align 1
228    ///   [3 bytes padding]
229    ///   port:      i32   offset 20, size 4, align 4
230    ///   total_size 24
231    pub fn connection_layout() -> StructLayout {
232        StructLayout {
233            name: "Connection".to_string(),
234            total_size: 24,
235            align: 8,
236            fields: vec![
237                Field {
238                    name: "is_active".into(),
239                    ty: TypeInfo::Primitive {
240                        name: "bool".into(),
241                        size: 1,
242                        align: 1,
243                    },
244                    offset: 0,
245                    size: 1,
246                    align: 1,
247                    source_file: None,
248                    source_line: None,
249                    access: AccessPattern::Unknown,
250                },
251                Field {
252                    name: "timeout".into(),
253                    ty: TypeInfo::Primitive {
254                        name: "f64".into(),
255                        size: 8,
256                        align: 8,
257                    },
258                    offset: 8,
259                    size: 8,
260                    align: 8,
261                    source_file: None,
262                    source_line: None,
263                    access: AccessPattern::Unknown,
264                },
265                Field {
266                    name: "is_tls".into(),
267                    ty: TypeInfo::Primitive {
268                        name: "bool".into(),
269                        size: 1,
270                        align: 1,
271                    },
272                    offset: 16,
273                    size: 1,
274                    align: 1,
275                    source_file: None,
276                    source_line: None,
277                    access: AccessPattern::Unknown,
278                },
279                Field {
280                    name: "port".into(),
281                    ty: TypeInfo::Primitive {
282                        name: "i32".into(),
283                        size: 4,
284                        align: 4,
285                    },
286                    offset: 20,
287                    size: 4,
288                    align: 4,
289                    source_file: None,
290                    source_line: None,
291                    access: AccessPattern::Unknown,
292                },
293            ],
294            source_file: None,
295            source_line: None,
296            arch: &X86_64_SYSV,
297            is_packed: false,
298            is_union: false,
299            is_repr_rust: false,
300            suppressed_findings: Vec::new(),
301            uncertain_fields: Vec::new(),
302        }
303    }
304
305    /// A perfectly packed layout (no padding anywhere).
306    pub fn packed_layout() -> StructLayout {
307        StructLayout {
308            name: "Packed".to_string(),
309            total_size: 8,
310            align: 4,
311            fields: vec![
312                Field {
313                    name: "a".into(),
314                    ty: TypeInfo::Primitive {
315                        name: "i32".into(),
316                        size: 4,
317                        align: 4,
318                    },
319                    offset: 0,
320                    size: 4,
321                    align: 4,
322                    source_file: None,
323                    source_line: None,
324                    access: AccessPattern::Unknown,
325                },
326                Field {
327                    name: "b".into(),
328                    ty: TypeInfo::Primitive {
329                        name: "i16".into(),
330                        size: 2,
331                        align: 2,
332                    },
333                    offset: 4,
334                    size: 2,
335                    align: 2,
336                    source_file: None,
337                    source_line: None,
338                    access: AccessPattern::Unknown,
339                },
340                Field {
341                    name: "c".into(),
342                    ty: TypeInfo::Primitive {
343                        name: "i16".into(),
344                        size: 2,
345                        align: 2,
346                    },
347                    offset: 6,
348                    size: 2,
349                    align: 2,
350                    source_file: None,
351                    source_line: None,
352                    access: AccessPattern::Unknown,
353                },
354            ],
355            source_file: None,
356            source_line: None,
357            arch: &X86_64_SYSV,
358            is_packed: false,
359            is_union: false,
360            is_repr_rust: false,
361            suppressed_findings: Vec::new(),
362            uncertain_fields: Vec::new(),
363        }
364    }
365
366    #[test]
367    fn test_find_padding_connection() {
368        let layout = connection_layout();
369        let gaps = find_padding(&layout);
370        assert_eq!(
371            gaps,
372            vec![
373                PaddingGap {
374                    after_field: "is_active".into(),
375                    bytes: 7,
376                    at_offset: 1
377                },
378                PaddingGap {
379                    after_field: "is_tls".into(),
380                    bytes: 3,
381                    at_offset: 17
382                },
383            ]
384        );
385    }
386
387    #[test]
388    fn test_find_padding_packed() {
389        let layout = packed_layout();
390        assert!(find_padding(&layout).is_empty());
391    }
392
393    #[test]
394    fn test_optimal_order() {
395        let layout = connection_layout();
396        let order: Vec<&str> = optimal_order(&layout)
397            .iter()
398            .map(|f| f.name.as_str())
399            .collect();
400        // timeout (align 8) first, then port (align 4), then bools (align 1)
401        assert_eq!(order[0], "timeout");
402        assert_eq!(order[1], "port");
403        assert!(order[2] == "is_active" || order[2] == "is_tls");
404    }
405}