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/// The type of a single field. Recursive for nested structs.
6#[derive(Debug, Clone)]
7pub enum TypeInfo {
8    Primitive {
9        name: String,
10        size: usize,
11        align: usize,
12    },
13    Pointer {
14        size: usize,
15        align: usize,
16    },
17    Array {
18        element: Box<TypeInfo>,
19        count: usize,
20        size: usize,
21        align: usize,
22    },
23    Struct(Box<StructLayout>),
24    Opaque {
25        name: String,
26        size: usize,
27        align: usize,
28    },
29}
30
31impl TypeInfo {
32    pub fn size(&self) -> usize {
33        match self {
34            TypeInfo::Primitive { size, .. } => *size,
35            TypeInfo::Pointer { size, .. } => *size,
36            TypeInfo::Array { size, .. } => *size,
37            TypeInfo::Struct(l) => l.total_size,
38            TypeInfo::Opaque { size, .. } => *size,
39        }
40    }
41
42    pub fn align(&self) -> usize {
43        match self {
44            TypeInfo::Primitive { align, .. } => *align,
45            TypeInfo::Pointer { align, .. } => *align,
46            TypeInfo::Array { align, .. } => *align,
47            TypeInfo::Struct(l) => l.align,
48            TypeInfo::Opaque { align, .. } => *align,
49        }
50    }
51}
52
53#[derive(Debug, Clone, PartialEq, serde::Serialize)]
54pub enum AccessPattern {
55    Unknown,
56    Concurrent {
57        guard: Option<String>,
58        is_atomic: bool,
59    },
60    ReadMostly,
61    Padding,
62}
63
64#[derive(Debug, Clone)]
65pub struct Field {
66    pub name: String,
67    pub ty: TypeInfo,
68    pub offset: usize,
69    pub size: usize,
70    pub align: usize,
71    pub source_file: Option<String>,
72    pub source_line: Option<u32>,
73    pub access: AccessPattern,
74}
75
76/// One complete struct as read from DWARF or source and enriched by analysis.
77#[derive(Debug, Clone)]
78pub struct StructLayout {
79    pub name: String,
80    pub total_size: usize,
81    pub align: usize,
82    pub fields: Vec<Field>,
83    pub source_file: Option<String>,
84    pub source_line: Option<u32>,
85    pub arch: &'static ArchConfig,
86    pub is_packed: bool,
87    /// True when this layout was parsed from a C/C++ `union` declaration.
88    /// All fields share the same base offset (0); analysis suppresses reorder
89    /// and padding findings that do not apply to unions.
90    pub is_union: bool,
91}
92
93#[derive(Debug, Clone, PartialEq, serde::Serialize)]
94pub struct PaddingGap {
95    pub after_field: String,
96    pub bytes: usize,
97    pub at_offset: usize,
98}
99
100#[derive(Debug, Clone, serde::Serialize)]
101pub struct SharingConflict {
102    pub fields: Vec<String>,
103    pub cache_line: usize,
104}
105
106/// Find all padding gaps between consecutive fields.
107///
108/// Returns an empty vec for union layouts — all fields share offset 0 by
109/// definition, so the concept of inter-field padding does not apply.
110pub fn find_padding(layout: &StructLayout) -> Vec<PaddingGap> {
111    if layout.is_union {
112        return Vec::new();
113    }
114    let mut gaps = Vec::new();
115    for window in layout.fields.windows(2) {
116        let current = &window[0];
117        let next = &window[1];
118        let end = current.offset + current.size;
119        if next.offset > end {
120            gaps.push(PaddingGap {
121                after_field: current.name.clone(),
122                bytes: next.offset - end,
123                at_offset: end,
124            });
125        }
126    }
127    // Trailing padding: struct total_size > last field end
128    if let Some(last) = layout.fields.last() {
129        let end = last.offset + last.size;
130        if layout.total_size > end {
131            gaps.push(PaddingGap {
132                after_field: last.name.clone(),
133                bytes: layout.total_size - end,
134                at_offset: end,
135            });
136        }
137    }
138    gaps
139}
140
141/// Return fields sorted by descending alignment then descending size (optimal order).
142pub fn optimal_order(layout: &StructLayout) -> Vec<&Field> {
143    let mut sorted: Vec<&Field> = layout.fields.iter().collect();
144    sorted.sort_by(|a, b| {
145        b.align
146            .cmp(&a.align)
147            .then(b.size.cmp(&a.size))
148            .then(a.name.cmp(&b.name))
149    });
150    sorted
151}
152
153// ── tests ─────────────────────────────────────────────────────────────────────
154
155#[cfg(any(test, feature = "test-helpers"))]
156pub mod test_fixtures {
157    use super::*;
158    use crate::arch::X86_64_SYSV;
159
160    /// The canonical misaligned layout used across crate tests.
161    ///   is_active: bool  offset 0,  size 1, align 1
162    ///   [7 bytes padding]
163    ///   timeout:   f64   offset 8,  size 8, align 8
164    ///   is_tls:    bool  offset 16, size 1, align 1
165    ///   [3 bytes padding]
166    ///   port:      i32   offset 20, size 4, align 4
167    ///   total_size 24
168    pub fn connection_layout() -> StructLayout {
169        StructLayout {
170            name: "Connection".to_string(),
171            total_size: 24,
172            align: 8,
173            fields: vec![
174                Field {
175                    name: "is_active".into(),
176                    ty: TypeInfo::Primitive {
177                        name: "bool".into(),
178                        size: 1,
179                        align: 1,
180                    },
181                    offset: 0,
182                    size: 1,
183                    align: 1,
184                    source_file: None,
185                    source_line: None,
186                    access: AccessPattern::Unknown,
187                },
188                Field {
189                    name: "timeout".into(),
190                    ty: TypeInfo::Primitive {
191                        name: "f64".into(),
192                        size: 8,
193                        align: 8,
194                    },
195                    offset: 8,
196                    size: 8,
197                    align: 8,
198                    source_file: None,
199                    source_line: None,
200                    access: AccessPattern::Unknown,
201                },
202                Field {
203                    name: "is_tls".into(),
204                    ty: TypeInfo::Primitive {
205                        name: "bool".into(),
206                        size: 1,
207                        align: 1,
208                    },
209                    offset: 16,
210                    size: 1,
211                    align: 1,
212                    source_file: None,
213                    source_line: None,
214                    access: AccessPattern::Unknown,
215                },
216                Field {
217                    name: "port".into(),
218                    ty: TypeInfo::Primitive {
219                        name: "i32".into(),
220                        size: 4,
221                        align: 4,
222                    },
223                    offset: 20,
224                    size: 4,
225                    align: 4,
226                    source_file: None,
227                    source_line: None,
228                    access: AccessPattern::Unknown,
229                },
230            ],
231            source_file: None,
232            source_line: None,
233            arch: &X86_64_SYSV,
234            is_packed: false,
235            is_union: false,
236        }
237    }
238
239    /// A perfectly packed layout (no padding anywhere).
240    pub fn packed_layout() -> StructLayout {
241        StructLayout {
242            name: "Packed".to_string(),
243            total_size: 8,
244            align: 4,
245            fields: vec![
246                Field {
247                    name: "a".into(),
248                    ty: TypeInfo::Primitive {
249                        name: "i32".into(),
250                        size: 4,
251                        align: 4,
252                    },
253                    offset: 0,
254                    size: 4,
255                    align: 4,
256                    source_file: None,
257                    source_line: None,
258                    access: AccessPattern::Unknown,
259                },
260                Field {
261                    name: "b".into(),
262                    ty: TypeInfo::Primitive {
263                        name: "i16".into(),
264                        size: 2,
265                        align: 2,
266                    },
267                    offset: 4,
268                    size: 2,
269                    align: 2,
270                    source_file: None,
271                    source_line: None,
272                    access: AccessPattern::Unknown,
273                },
274                Field {
275                    name: "c".into(),
276                    ty: TypeInfo::Primitive {
277                        name: "i16".into(),
278                        size: 2,
279                        align: 2,
280                    },
281                    offset: 6,
282                    size: 2,
283                    align: 2,
284                    source_file: None,
285                    source_line: None,
286                    access: AccessPattern::Unknown,
287                },
288            ],
289            source_file: None,
290            source_line: None,
291            arch: &X86_64_SYSV,
292            is_packed: false,
293            is_union: false,
294        }
295    }
296
297    #[test]
298    fn test_find_padding_connection() {
299        let layout = connection_layout();
300        let gaps = find_padding(&layout);
301        assert_eq!(
302            gaps,
303            vec![
304                PaddingGap {
305                    after_field: "is_active".into(),
306                    bytes: 7,
307                    at_offset: 1
308                },
309                PaddingGap {
310                    after_field: "is_tls".into(),
311                    bytes: 3,
312                    at_offset: 17
313                },
314            ]
315        );
316    }
317
318    #[test]
319    fn test_find_padding_packed() {
320        let layout = packed_layout();
321        assert!(find_padding(&layout).is_empty());
322    }
323
324    #[test]
325    fn test_optimal_order() {
326        let layout = connection_layout();
327        let order: Vec<&str> = optimal_order(&layout)
328            .iter()
329            .map(|f| f.name.as_str())
330            .collect();
331        // timeout (align 8) first, then port (align 4), then bools (align 1)
332        assert_eq!(order[0], "timeout");
333        assert_eq!(order[1], "port");
334        assert!(order[2] == "is_active" || order[2] == "is_tls");
335    }
336}