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
136#[derive(Debug, Clone, PartialEq, serde::Serialize)]
137pub struct PaddingGap {
138    pub after_field: String,
139    pub bytes: usize,
140    pub at_offset: usize,
141}
142
143#[derive(Debug, Clone, serde::Serialize)]
144pub struct SharingConflict {
145    pub fields: Vec<String>,
146    pub cache_line: usize,
147}
148
149/// Find all padding gaps between consecutive fields.
150///
151/// Returns an empty vec for union layouts — all fields share offset 0 by
152/// definition, so the concept of inter-field padding does not apply.
153pub fn find_padding(layout: &StructLayout) -> Vec<PaddingGap> {
154    if layout.is_union {
155        return Vec::new();
156    }
157    let mut gaps = Vec::new();
158    for window in layout.fields.windows(2) {
159        let current = &window[0];
160        let next = &window[1];
161        let end = current.offset + current.size;
162        if next.offset > end {
163            gaps.push(PaddingGap {
164                after_field: current.name.clone(),
165                bytes: next.offset - end,
166                at_offset: end,
167            });
168        }
169    }
170    // Trailing padding: struct total_size > last field end
171    if let Some(last) = layout.fields.last() {
172        let end = last.offset + last.size;
173        if layout.total_size > end {
174            gaps.push(PaddingGap {
175                after_field: last.name.clone(),
176                bytes: layout.total_size - end,
177                at_offset: end,
178            });
179        }
180    }
181    gaps
182}
183
184/// Return fields sorted by descending alignment then descending size (optimal order).
185pub fn optimal_order(layout: &StructLayout) -> Vec<&Field> {
186    let mut sorted: Vec<&Field> = layout.fields.iter().collect();
187    sorted.sort_by(|a, b| {
188        b.align
189            .cmp(&a.align)
190            .then(b.size.cmp(&a.size))
191            .then(a.name.cmp(&b.name))
192    });
193    sorted
194}
195
196// ── tests ─────────────────────────────────────────────────────────────────────
197
198#[cfg(any(test, feature = "test-helpers"))]
199pub mod test_fixtures {
200    use super::*;
201    use crate::arch::X86_64_SYSV;
202
203    /// The canonical misaligned layout used across crate tests.
204    ///   is_active: bool  offset 0,  size 1, align 1
205    ///   [7 bytes padding]
206    ///   timeout:   f64   offset 8,  size 8, align 8
207    ///   is_tls:    bool  offset 16, size 1, align 1
208    ///   [3 bytes padding]
209    ///   port:      i32   offset 20, size 4, align 4
210    ///   total_size 24
211    pub fn connection_layout() -> StructLayout {
212        StructLayout {
213            name: "Connection".to_string(),
214            total_size: 24,
215            align: 8,
216            fields: vec![
217                Field {
218                    name: "is_active".into(),
219                    ty: TypeInfo::Primitive {
220                        name: "bool".into(),
221                        size: 1,
222                        align: 1,
223                    },
224                    offset: 0,
225                    size: 1,
226                    align: 1,
227                    source_file: None,
228                    source_line: None,
229                    access: AccessPattern::Unknown,
230                },
231                Field {
232                    name: "timeout".into(),
233                    ty: TypeInfo::Primitive {
234                        name: "f64".into(),
235                        size: 8,
236                        align: 8,
237                    },
238                    offset: 8,
239                    size: 8,
240                    align: 8,
241                    source_file: None,
242                    source_line: None,
243                    access: AccessPattern::Unknown,
244                },
245                Field {
246                    name: "is_tls".into(),
247                    ty: TypeInfo::Primitive {
248                        name: "bool".into(),
249                        size: 1,
250                        align: 1,
251                    },
252                    offset: 16,
253                    size: 1,
254                    align: 1,
255                    source_file: None,
256                    source_line: None,
257                    access: AccessPattern::Unknown,
258                },
259                Field {
260                    name: "port".into(),
261                    ty: TypeInfo::Primitive {
262                        name: "i32".into(),
263                        size: 4,
264                        align: 4,
265                    },
266                    offset: 20,
267                    size: 4,
268                    align: 4,
269                    source_file: None,
270                    source_line: None,
271                    access: AccessPattern::Unknown,
272                },
273            ],
274            source_file: None,
275            source_line: None,
276            arch: &X86_64_SYSV,
277            is_packed: false,
278            is_union: false,
279            is_repr_rust: false,
280            suppressed_findings: Vec::new(),
281        }
282    }
283
284    /// A perfectly packed layout (no padding anywhere).
285    pub fn packed_layout() -> StructLayout {
286        StructLayout {
287            name: "Packed".to_string(),
288            total_size: 8,
289            align: 4,
290            fields: vec![
291                Field {
292                    name: "a".into(),
293                    ty: TypeInfo::Primitive {
294                        name: "i32".into(),
295                        size: 4,
296                        align: 4,
297                    },
298                    offset: 0,
299                    size: 4,
300                    align: 4,
301                    source_file: None,
302                    source_line: None,
303                    access: AccessPattern::Unknown,
304                },
305                Field {
306                    name: "b".into(),
307                    ty: TypeInfo::Primitive {
308                        name: "i16".into(),
309                        size: 2,
310                        align: 2,
311                    },
312                    offset: 4,
313                    size: 2,
314                    align: 2,
315                    source_file: None,
316                    source_line: None,
317                    access: AccessPattern::Unknown,
318                },
319                Field {
320                    name: "c".into(),
321                    ty: TypeInfo::Primitive {
322                        name: "i16".into(),
323                        size: 2,
324                        align: 2,
325                    },
326                    offset: 6,
327                    size: 2,
328                    align: 2,
329                    source_file: None,
330                    source_line: None,
331                    access: AccessPattern::Unknown,
332                },
333            ],
334            source_file: None,
335            source_line: None,
336            arch: &X86_64_SYSV,
337            is_packed: false,
338            is_union: false,
339            is_repr_rust: false,
340            suppressed_findings: Vec::new(),
341        }
342    }
343
344    #[test]
345    fn test_find_padding_connection() {
346        let layout = connection_layout();
347        let gaps = find_padding(&layout);
348        assert_eq!(
349            gaps,
350            vec![
351                PaddingGap {
352                    after_field: "is_active".into(),
353                    bytes: 7,
354                    at_offset: 1
355                },
356                PaddingGap {
357                    after_field: "is_tls".into(),
358                    bytes: 3,
359                    at_offset: 17
360                },
361            ]
362        );
363    }
364
365    #[test]
366    fn test_find_padding_packed() {
367        let layout = packed_layout();
368        assert!(find_padding(&layout).is_empty());
369    }
370
371    #[test]
372    fn test_optimal_order() {
373        let layout = connection_layout();
374        let order: Vec<&str> = optimal_order(&layout)
375            .iter()
376            .map(|f| f.name.as_str())
377            .collect();
378        // timeout (align 8) first, then port (align 4), then bools (align 1)
379        assert_eq!(order[0], "timeout");
380        assert_eq!(order[1], "port");
381        assert!(order[2] == "is_active" || order[2] == "is_tls");
382    }
383}