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