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