Skip to main content

waterui_cli/workflows/
elf.rs

1//! ELF `PT_LOAD` alignment checks for packaged shared libraries.
2//!
3//! Android 15 devices with 16 KB page sizes — and Google Play's 2025
4//! requirement — reject a package in which any shared library maps a `LOAD`
5//! segment with an alignment below the page size. Every `.so` the CLI stages
6//! into an Android package is checked here so a misaligned artifact fails the
7//! build naming the file, rather than surfacing on the device as an install
8//! error or a compatibility dialog.
9
10use std::path::Path;
11
12use eyre::bail;
13
14/// The alignment every packaged `LOAD` segment must reach: 16 KB, the largest
15/// page size Android ships with.
16pub const REQUIRED_LOAD_ALIGNMENT: u64 = 0x4000;
17
18/// Require every `PT_LOAD` segment in the ELF at `path` to declare an
19/// alignment of at least [`REQUIRED_LOAD_ALIGNMENT`].
20///
21/// An unreadable or unparsable file is an error: a staged library that cannot
22/// be inspected cannot be trusted to load.
23///
24/// # Errors
25/// Returns an error when the file cannot be read or parsed as ELF, or when
26/// any `PT_LOAD` segment's alignment is below the requirement.
27pub fn require_aligned_load_segments(path: &Path) -> eyre::Result<()> {
28    let data = std::fs::read(path).map_err(|error| {
29        eyre::eyre!(
30            "failed to read staged library {} for ELF alignment validation: {error}",
31            path.display()
32        )
33    })?;
34    check_load_segment_alignment(&data)
35        .map_err(|error| eyre::eyre!("staged library {}: {error}", path.display()))
36}
37
38/// Require every `*.so` directly inside `directory` to satisfy
39/// [`require_aligned_load_segments`].
40///
41/// # Errors
42/// Returns an error when the directory cannot be read or any library inside
43/// fails the alignment check.
44pub async fn require_aligned_shared_libraries(directory: &Path) -> eyre::Result<()> {
45    let directory = directory.to_path_buf();
46    smol::unblock(move || {
47        for entry in std::fs::read_dir(&directory)? {
48            let path = entry?.path();
49            if path.extension() != Some(std::ffi::OsStr::new("so")) {
50                continue;
51            }
52            require_aligned_load_segments(&path)?;
53        }
54        Ok(())
55    })
56    .await
57}
58
59/// `Ok(())` when every `PT_LOAD` segment of the ELF image is aligned to at
60/// least [`REQUIRED_LOAD_ALIGNMENT`]; an error naming the offending segment
61/// otherwise.
62fn check_load_segment_alignment(data: &[u8]) -> eyre::Result<()> {
63    use object::read::elf::{ElfFile, FileHeader, ProgramHeader as _};
64
65    fn scan<Elf>(data: &[u8]) -> eyre::Result<()>
66    where
67        Elf: FileHeader<Endian = object::Endianness>,
68    {
69        let file = ElfFile::<Elf>::parse(data)
70            .map_err(|error| eyre::eyre!("not a parseable ELF file: {error}"))?;
71        let endian = file.endian();
72        let headers = file
73            .elf_header()
74            .program_headers(endian, data)
75            .map_err(|error| eyre::eyre!("cannot read ELF program headers: {error}"))?;
76        for header in headers {
77            if header.p_type(endian) != object::elf::PT_LOAD {
78                continue;
79            }
80            let align: u64 = header.p_align(endian).into();
81            if align < REQUIRED_LOAD_ALIGNMENT {
82                let offset: u64 = header.p_offset(endian).into();
83                bail!(
84                    "PT_LOAD segment (offset {offset:#x}) is aligned to {align:#x}; \
85                     Android 16 KB page-size support requires at least {REQUIRED_LOAD_ALIGNMENT:#x}",
86                );
87            }
88        }
89        Ok(())
90    }
91
92    match data.get(4) {
93        Some(&object::elf::ELFCLASS64) => {
94            scan::<object::elf::FileHeader64<object::Endianness>>(data)
95        }
96        Some(&object::elf::ELFCLASS32) => {
97            scan::<object::elf::FileHeader32<object::Endianness>>(data)
98        }
99        _ => bail!("not a parseable ELF file: missing or invalid ELF class byte"),
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::{
106        REQUIRED_LOAD_ALIGNMENT, check_load_segment_alignment, require_aligned_load_segments,
107    };
108
109    /// Minimal ELF64 header + one program header with a chosen `p_align`,
110    /// enough for `object` to enumerate segments.
111    fn elf_with_load_align(align: u64) -> Vec<u8> {
112        let mut data = vec![0u8; 0x100];
113        data[..4].copy_from_slice(b"\x7fELF");
114        data[4] = 2; // ELFCLASS64
115        data[5] = 1; // little-endian
116        data[6] = 1; // EI_VERSION (EV_CURRENT)
117        data[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN
118        data[18..20].copy_from_slice(&0xb7u16.to_le_bytes()); // EM_AARCH64
119        data[20..24].copy_from_slice(&1u32.to_le_bytes()); // version
120        data[32..40].copy_from_slice(&64u64.to_le_bytes()); // e_phoff
121        data[52..54].copy_from_slice(&64u16.to_le_bytes()); // e_ehsize
122        data[54..56].copy_from_slice(&56u16.to_le_bytes()); // e_phentsize
123        data[56..58].copy_from_slice(&1u16.to_le_bytes()); // e_phnum
124        // program header at offset 64
125        data[64..68].copy_from_slice(&1u32.to_le_bytes()); // PT_LOAD
126        data[68..72].copy_from_slice(&5u32.to_le_bytes()); // flags
127        data[72..80].copy_from_slice(&0u64.to_le_bytes()); // p_offset
128        data[112..120].copy_from_slice(&align.to_le_bytes()); // p_align
129        data
130    }
131
132    #[test]
133    fn accepts_a_16k_aligned_load_segment() {
134        check_load_segment_alignment(&elf_with_load_align(REQUIRED_LOAD_ALIGNMENT))
135            .expect("16 KB-aligned LOAD must pass");
136        check_load_segment_alignment(&elf_with_load_align(0x10000))
137            .expect("larger alignment must pass");
138    }
139
140    #[test]
141    fn rejects_a_4k_aligned_load_segment() {
142        let error = check_load_segment_alignment(&elf_with_load_align(0x1000))
143            .expect_err("4 KB-aligned LOAD must fail");
144        let message = error.to_string();
145        assert!(
146            message.contains("0x1000"),
147            "message names the alignment: {message}"
148        );
149        assert!(
150            message.contains("0x4000"),
151            "message names the requirement: {message}"
152        );
153    }
154
155    #[test]
156    fn rejects_non_elf_data() {
157        let _ = check_load_segment_alignment(b"not an elf").expect_err("non-ELF must fail");
158    }
159
160    #[test]
161    fn require_aligned_load_segments_reports_the_file() {
162        let dir = tempfile::tempdir().expect("tempdir");
163        let path = dir.path().join("libbad.so");
164        std::fs::write(&path, elf_with_load_align(0x1000)).expect("write");
165        let error = require_aligned_load_segments(&path).expect_err("must fail");
166        assert!(error.to_string().contains("libbad.so"));
167    }
168}