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