Skip to main content

ud_format/
solana.rs

1//! Solana on-chain program loader-state stripping.
2//!
3//! A Solana program's ELF bytes don't sit at the start of
4//! its on-chain account — they're wrapped in a per-loader
5//! header that the runtime parses before handing the ELF to
6//! the verifier. Three loaders are in active use:
7//!
8//! * **`BPFLoader2111…`** (non-upgradeable, legacy). The
9//!   program's account data IS the ELF; nothing to strip.
10//! * **`BPFLoaderUpgradeab1e…`** (today's default). Two
11//!   accounts: a *Program* account whose data is a 36-byte
12//!   `UpgradeableLoaderState::Program` (a 4-byte Borsh enum
13//!   tag of value 2, followed by the 32-byte ProgramData
14//!   pubkey), and a *ProgramData* account whose data is an
15//!   `UpgradeableLoaderState::ProgramData` header followed
16//!   by the ELF. The ProgramData header is **45 bytes** when
17//!   an upgrade authority is set or **13 bytes** when it's
18//!   `None` (the difference is the 32-byte authority pubkey
19//!   carried under a `Some` variant of `Option<Pubkey>`).
20//! * **`LoaderV411…`** (Agave's newer loader). A
21//!   `LoaderV4State` of 48 bytes (8 slot + 32 authority + 8
22//!   status repr) followed by the ELF.
23//!
24//! Each stripping function verifies the ELF magic
25//! (`\x7fELF`) immediately past the header so a mismatched
26//! layout fails loudly rather than producing garbage. The
27//! upgradeable case has a small subtlety: when one variant
28//! (45-byte header) doesn't expose the magic, we try the
29//! other (13-byte) before giving up, since the option-tag
30//! byte at offset 12 can lie in unusual encoder paths.
31//!
32//! This module is the single source of truth for those
33//! byte-level decisions. Two callers consume it:
34//!
35//! * [`ud_cli::solana`] — adds the network layer (JSON-RPC
36//!   over `ureq` + base64 + on-disk cache) on top.
37//! * [`ud_wasm`] — exposes the strippers as wasm-bindgen
38//!   functions so the browser playground can do the network
39//!   piece in JS and call into here for byte parsing.
40
41use crate::elf::ELF_MAGIC;
42
43/// Loader program ID (base58) — `BPFLoader2111…`,
44/// non-upgradeable, legacy.
45pub const BPF_LOADER_V2: &str = "BPFLoader2111111111111111111111111111111111";
46
47/// Loader program ID (base58) — `BPFLoaderUpgradeab1e…`,
48/// today's default.
49pub const BPF_LOADER_UPGRADEABLE: &str = "BPFLoaderUpgradeab1e11111111111111111111111";
50
51/// Loader program ID (base58) — `LoaderV411…`, Agave's
52/// newer loader.
53pub const LOADER_V4: &str = "LoaderV411111111111111111111111111111111111";
54
55/// Borsh enum tag for `UpgradeableLoaderState::Program`.
56const UPGRADEABLE_STATE_PROGRAM: u32 = 2;
57/// Borsh enum tag for `UpgradeableLoaderState::ProgramData`.
58const UPGRADEABLE_STATE_PROGRAM_DATA: u32 = 3;
59
60/// `UpgradeableLoaderState::ProgramData` header size when
61/// the `upgrade_authority_address: Option<Pubkey>` is
62/// `Some`: 4 (enum tag) + 8 (slot) + 1 (option tag) + 32
63/// (pubkey) = 45.
64const PROGRAMDATA_HEADER_WITH_AUTH: usize = 45;
65
66/// Same header when the authority is `None`:
67/// 4 (enum tag) + 8 (slot) + 1 (option tag) = 13 bytes.
68const PROGRAMDATA_HEADER_NO_AUTH: usize = 13;
69
70/// `LoaderV4State` is 48 bytes: 8 (slot) + 32 (authority or
71/// next-version pubkey) + 8 (`LoaderV4Status` repr).
72const LOADER_V4_HEADER: usize = 48;
73
74/// Which loader manages a given program account. The
75/// classifier matches against the base58-encoded owner
76/// pubkey as it comes out of the Solana JSON-RPC response;
77/// callers don't need to decode to raw bytes first.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum LoaderKind {
80    /// `BPFLoader2111…` — account data is the ELF directly.
81    BpfLoader2,
82    /// `BPFLoaderUpgradeab1e…` — Program + ProgramData split.
83    Upgradeable,
84    /// `LoaderV411…` — 48-byte header then ELF.
85    LoaderV4,
86    /// Owner doesn't match any known loader. Caller should
87    /// surface the actual owner string to the user.
88    Unknown,
89}
90
91#[derive(Debug, thiserror::Error)]
92pub enum Error {
93    #[error("account data too short for {loader}: need ≥{need} bytes, got {got}")]
94    TooShort {
95        loader: &'static str,
96        need: usize,
97        got: usize,
98    },
99    #[error("ELF magic missing at expected offset {offset} for {loader}")]
100    NotElf { loader: &'static str, offset: usize },
101    #[error("expected {loader} Borsh enum tag {expected} at offset 0, got {got}")]
102    BadEnumTag {
103        loader: &'static str,
104        expected: u32,
105        got: u32,
106    },
107    #[error(
108        "ProgramData has neither {with_auth}-byte nor {no_auth}-byte header that exposes ELF magic"
109    )]
110    AmbiguousProgramData { with_auth: usize, no_auth: usize },
111}
112
113/// Match a base58-encoded owner pubkey against the known
114/// loader IDs.
115#[must_use]
116pub fn classify_loader(owner: &str) -> LoaderKind {
117    match owner {
118        BPF_LOADER_V2 => LoaderKind::BpfLoader2,
119        BPF_LOADER_UPGRADEABLE => LoaderKind::Upgradeable,
120        LOADER_V4 => LoaderKind::LoaderV4,
121        _ => LoaderKind::Unknown,
122    }
123}
124
125/// For a Program account whose owner is the upgradeable
126/// loader, return the 32-byte ProgramData address embedded
127/// in the account data. The caller fetches that account
128/// next.
129///
130/// Layout: `UpgradeableLoaderState::Program` = 4 bytes
131/// Borsh enum tag (value 2) + 32 bytes pubkey = 36 bytes
132/// total.
133pub fn programdata_pubkey(program_data: &[u8]) -> Result<[u8; 32], Error> {
134    const NEEDED: usize = 36;
135    if program_data.len() < NEEDED {
136        return Err(Error::TooShort {
137            loader: "BPFLoaderUpgradeable Program",
138            need: NEEDED,
139            got: program_data.len(),
140        });
141    }
142    let tag = u32::from_le_bytes([
143        program_data[0],
144        program_data[1],
145        program_data[2],
146        program_data[3],
147    ]);
148    if tag != UPGRADEABLE_STATE_PROGRAM {
149        return Err(Error::BadEnumTag {
150            loader: "BPFLoaderUpgradeable Program",
151            expected: UPGRADEABLE_STATE_PROGRAM,
152            got: tag,
153        });
154    }
155    let mut pubkey = [0u8; 32];
156    pubkey.copy_from_slice(&program_data[4..36]);
157    Ok(pubkey)
158}
159
160/// Strip the loader-state from a `BPFLoader2` account.
161/// There's no header — the bytes are the ELF directly.
162/// Verifies the ELF magic so a malformed account fails
163/// loudly.
164pub fn strip_bpf_loader_v2(data: &[u8]) -> Result<&[u8], Error> {
165    verify_elf(data, "BPFLoader2", 0)?;
166    Ok(data)
167}
168
169/// Strip the `UpgradeableLoaderState::ProgramData` header
170/// off the ProgramData account's data. Returns a slice into
171/// the input (no copy).
172///
173/// Tries the 45-byte (with-authority) variant first; if
174/// that doesn't expose ELF magic, falls back to the 13-byte
175/// (no-authority) variant before giving up.
176pub fn strip_bpf_loader_upgradeable(programdata: &[u8]) -> Result<&[u8], Error> {
177    if programdata.len() < PROGRAMDATA_HEADER_NO_AUTH {
178        return Err(Error::TooShort {
179            loader: "BPFLoaderUpgradeable ProgramData",
180            need: PROGRAMDATA_HEADER_NO_AUTH,
181            got: programdata.len(),
182        });
183    }
184    let tag = u32::from_le_bytes([
185        programdata[0],
186        programdata[1],
187        programdata[2],
188        programdata[3],
189    ]);
190    if tag != UPGRADEABLE_STATE_PROGRAM_DATA {
191        return Err(Error::BadEnumTag {
192            loader: "BPFLoaderUpgradeable ProgramData",
193            expected: UPGRADEABLE_STATE_PROGRAM_DATA,
194            got: tag,
195        });
196    }
197    // Option<Pubkey> byte at offset 12: 1 = Some, 0 = None.
198    let auth_present = programdata.get(12).copied().unwrap_or(0) != 0;
199    let primary = if auth_present {
200        PROGRAMDATA_HEADER_WITH_AUTH
201    } else {
202        PROGRAMDATA_HEADER_NO_AUTH
203    };
204    if has_elf_at(programdata, primary) {
205        return Ok(&programdata[primary..]);
206    }
207    let alt = if auth_present {
208        PROGRAMDATA_HEADER_NO_AUTH
209    } else {
210        PROGRAMDATA_HEADER_WITH_AUTH
211    };
212    if has_elf_at(programdata, alt) {
213        return Ok(&programdata[alt..]);
214    }
215    Err(Error::AmbiguousProgramData {
216        with_auth: PROGRAMDATA_HEADER_WITH_AUTH,
217        no_auth: PROGRAMDATA_HEADER_NO_AUTH,
218    })
219}
220
221/// Strip the 48-byte `LoaderV4State` header. Returns a
222/// slice into the input.
223pub fn strip_loader_v4(data: &[u8]) -> Result<&[u8], Error> {
224    if data.len() < LOADER_V4_HEADER + ELF_MAGIC.len() {
225        return Err(Error::TooShort {
226            loader: "LoaderV4",
227            need: LOADER_V4_HEADER + ELF_MAGIC.len(),
228            got: data.len(),
229        });
230    }
231    verify_elf(&data[LOADER_V4_HEADER..], "LoaderV4", LOADER_V4_HEADER)?;
232    Ok(&data[LOADER_V4_HEADER..])
233}
234
235fn has_elf_at(data: &[u8], offset: usize) -> bool {
236    data.len() >= offset + ELF_MAGIC.len() && data[offset..offset + ELF_MAGIC.len()] == ELF_MAGIC
237}
238
239fn verify_elf(bytes: &[u8], loader: &'static str, offset: usize) -> Result<(), Error> {
240    if bytes.len() < ELF_MAGIC.len() || bytes[..ELF_MAGIC.len()] != ELF_MAGIC {
241        return Err(Error::NotElf { loader, offset });
242    }
243    Ok(())
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn elf_bytes(extra: usize) -> Vec<u8> {
251        let mut v = Vec::with_capacity(ELF_MAGIC.len() + extra);
252        v.extend_from_slice(&ELF_MAGIC);
253        v.resize(ELF_MAGIC.len() + extra, 0);
254        v
255    }
256
257    #[test]
258    fn classify_recognises_the_three_loaders() {
259        assert_eq!(classify_loader(BPF_LOADER_V2), LoaderKind::BpfLoader2);
260        assert_eq!(
261            classify_loader(BPF_LOADER_UPGRADEABLE),
262            LoaderKind::Upgradeable
263        );
264        assert_eq!(classify_loader(LOADER_V4), LoaderKind::LoaderV4);
265        assert_eq!(
266            classify_loader("11111111111111111111111111111111"),
267            LoaderKind::Unknown
268        );
269    }
270
271    #[test]
272    fn strip_bpf_loader_v2_is_identity_with_elf_check() {
273        let bytes = elf_bytes(64);
274        assert_eq!(strip_bpf_loader_v2(&bytes).unwrap(), &bytes[..]);
275    }
276
277    #[test]
278    fn strip_bpf_loader_v2_rejects_non_elf() {
279        let mut bytes = elf_bytes(64);
280        bytes[0] = 0; // corrupt magic
281        assert!(matches!(
282            strip_bpf_loader_v2(&bytes),
283            Err(Error::NotElf { offset: 0, .. })
284        ));
285    }
286
287    #[test]
288    fn programdata_pubkey_extracts_the_32_byte_address() {
289        let mut buf = [0u8; 36];
290        buf[0..4].copy_from_slice(&UPGRADEABLE_STATE_PROGRAM.to_le_bytes());
291        for (i, b) in (0u8..32u8).enumerate() {
292            buf[4 + i] = b;
293        }
294        let pk = programdata_pubkey(&buf).unwrap();
295        let expected: [u8; 32] = std::array::from_fn(|i| u8::try_from(i).unwrap());
296        assert_eq!(pk, expected);
297    }
298
299    #[test]
300    fn programdata_pubkey_rejects_short_data() {
301        assert!(matches!(
302            programdata_pubkey(&[0u8; 20]),
303            Err(Error::TooShort { .. })
304        ));
305    }
306
307    #[test]
308    fn programdata_pubkey_rejects_wrong_tag() {
309        let buf = [0u8; 36]; // tag = 0, not 2
310        assert!(matches!(
311            programdata_pubkey(&buf),
312            Err(Error::BadEnumTag { .. })
313        ));
314    }
315
316    #[test]
317    fn strip_upgradeable_with_authority_header() {
318        let mut buf = Vec::new();
319        buf.extend_from_slice(&UPGRADEABLE_STATE_PROGRAM_DATA.to_le_bytes()); // 4
320        buf.extend_from_slice(&[0u8; 8]); // slot (12 bytes total so far)
321        buf.push(1); // option tag = Some (13 bytes total)
322        buf.extend_from_slice(&[0u8; 32]); // pubkey (45 bytes total)
323        let payload = elf_bytes(32);
324        buf.extend_from_slice(&payload);
325        let stripped = strip_bpf_loader_upgradeable(&buf).unwrap();
326        assert_eq!(stripped, &payload[..]);
327    }
328
329    #[test]
330    fn strip_upgradeable_without_authority_header() {
331        let mut buf = Vec::new();
332        buf.extend_from_slice(&UPGRADEABLE_STATE_PROGRAM_DATA.to_le_bytes());
333        buf.extend_from_slice(&[0u8; 8]);
334        buf.push(0); // None → 13-byte header
335        let payload = elf_bytes(32);
336        buf.extend_from_slice(&payload);
337        let stripped = strip_bpf_loader_upgradeable(&buf).unwrap();
338        assert_eq!(stripped, &payload[..]);
339    }
340
341    #[test]
342    fn strip_upgradeable_fallback_when_option_tag_lies() {
343        // Option tag says "Some" but the actual bytes have
344        // an ELF at offset 13 — meaning the encoder
345        // actually wrote the no-authority shape. The
346        // fallback path should still succeed.
347        let mut buf = Vec::new();
348        buf.extend_from_slice(&UPGRADEABLE_STATE_PROGRAM_DATA.to_le_bytes());
349        buf.extend_from_slice(&[0u8; 8]);
350        buf.push(1); // option tag = Some, but...
351        let payload = elf_bytes(32);
352        buf.extend_from_slice(&payload); // ...ELF starts at offset 13
353        let stripped = strip_bpf_loader_upgradeable(&buf).unwrap();
354        assert_eq!(stripped, &payload[..]);
355    }
356
357    #[test]
358    fn strip_loader_v4_removes_48_byte_header() {
359        let mut buf = vec![0u8; LOADER_V4_HEADER];
360        let payload = elf_bytes(32);
361        buf.extend_from_slice(&payload);
362        let stripped = strip_loader_v4(&buf).unwrap();
363        assert_eq!(stripped, &payload[..]);
364    }
365
366    #[test]
367    fn strip_loader_v4_rejects_short_account() {
368        assert!(matches!(
369            strip_loader_v4(&[0u8; 20]),
370            Err(Error::TooShort { .. })
371        ));
372    }
373}