Skip to main content

program/
program.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, anyhow, bail};
6use elf_parse::{Elf64Headers, SegmentType};
7use fidl_next::{Request, Responder, ServerEnd};
8use fidl_next_fuchsia_ldsvc as fuchsia_ldsvc;
9use fuchsia_bootfs::BootfsParser;
10use fuchsia_ldsvc::loader::{Clone, Config, LoadObject};
11use fuchsia_ldsvc::{Loader, LoaderLoadObjectResponse, LoaderServerHandler};
12use fuchsia_runtime::{HandleInfo, HandleType};
13use process_builder::elf_load::load_elf;
14use process_builder::{Message, MessageContents, StartupHandle, compute_initial_stack_pointer};
15use std::ffi::CString;
16use std::fmt::Write as _;
17use std::str;
18use zx::sys::{ZX_RSRC_SYSTEM_POWER_BASE, ZX_RSRC_SYSTEM_VMEX_BASE};
19use zx::{
20    Channel, DebugLog, Job, Name, NullableHandle, ObjectType, Process, ProcessOptions, Resource,
21    ResourceKind, Rights, Socket, Status, Vmar, VmarFlags, Vmo, VmoChildOptions,
22};
23use zx_libc::sanitizer::Log;
24
25// Defined locally here rather than imported from `fdio_sys` because userboot runs before the
26// FDIO/POSIX environment exists and does not depend on the fdio crate.
27const FDIO_FLAG_USE_FOR_STDIO: u16 = 0x8000;
28
29/// Represents either a DebugLog or a Socket logging handle.
30pub enum SystemLog {
31    /// Logging using a kernel DebugLog handle.
32    DebugLog(DebugLog),
33    /// Logging using a Socket handle.
34    Socket(Socket),
35}
36
37impl SystemLog {
38    fn to_handle(&self, rights: Rights) -> Result<NullableHandle, Status> {
39        match self {
40            Self::DebugLog(h) => h.duplicate_handle(rights).map(|h| h.into_handle()),
41            Self::Socket(h) => h.duplicate_handle(rights).map(|h| h.into_handle()),
42        }
43    }
44
45    fn duplicate(&self, rights: Rights) -> Result<Self, Status> {
46        match self {
47            Self::DebugLog(h) => Ok(Self::DebugLog(h.duplicate_handle(rights)?)),
48            Self::Socket(h) => Ok(Self::Socket(h.duplicate_handle(rights)?)),
49        }
50    }
51}
52
53/// Essential system handles extracted from the bootstrap capabilities message
54/// required to setup and launch the next program.
55pub struct SystemHandles {
56    /// Root job handle for creating child processes and default jobs.
57    pub root_job: Job,
58    /// VMEX resource handle for creating executable VMOs.
59    pub vmex_resource: Resource,
60    /// Power resource handle for power off / shutdown.
61    pub power_resource: Option<Resource>,
62    /// Handle to the stable vDSO VMO.
63    pub vdso_vmo: Vmo,
64    /// Handle to the ZBI VMO containing boot payload.
65    pub zbi_vmo: Vmo,
66    /// Handle to the debug log or log socket.
67    pub log: SystemLog,
68    /// Additional startup handles (resources, vDSOs, kernel files) to pass to the child process.
69    pub startup_handles: Vec<StartupHandle>,
70}
71
72impl SystemHandles {
73    /// Extracts essential system handles from the provided handle iterator.
74    pub fn from_handles(handles: impl IntoIterator<Item = zx::HandleInfo>) -> Result<Self, Error> {
75        let mut root_job = None;
76        let mut vmex_resource = None;
77        let mut power_resource = None;
78        let mut vdso_vmo = None;
79        let mut zbi_vmo = None;
80        let mut log = None;
81
82        let mut startup_handles = Vec::new();
83        let mut vdso_count = 0u16;
84        let mut kernel_file_count = 0u16;
85
86        for zx::HandleInfo { object_type, handle, .. } in handles {
87            match (object_type, handle.get_name()) {
88                (ObjectType::JOB, Ok(name)) => {
89                    if name == "root" {
90                        root_job = Some(Job::from(handle));
91                    }
92                }
93                (ObjectType::RESOURCE, Ok(name)) => {
94                    let htype = if name == "mmio" {
95                        HandleType::MmioResource
96                    } else if name == "irq" {
97                        HandleType::IrqResource
98                    } else if name == "io_port" {
99                        HandleType::IoportResource
100                    } else if name == "smc" {
101                        HandleType::SmcResource
102                    } else if name == "system" {
103                        let resource = handle.as_handle_ref().cast::<Resource>();
104                        if vmex_resource.is_none() {
105                            vmex_resource = resource
106                                .create_child(
107                                    ResourceKind::SYSTEM,
108                                    None,
109                                    ZX_RSRC_SYSTEM_VMEX_BASE,
110                                    1,
111                                    b"vmex",
112                                )
113                                .ok();
114                        }
115                        if power_resource.is_none() {
116                            power_resource = resource
117                                .create_child(
118                                    ResourceKind::SYSTEM,
119                                    None,
120                                    ZX_RSRC_SYSTEM_POWER_BASE,
121                                    1,
122                                    b"power",
123                                )
124                                .ok();
125                        }
126                        HandleType::SystemResource
127                    } else if name == "vmex" {
128                        vmex_resource = Some(Resource::from(handle));
129                        continue;
130                    } else if name == "power" {
131                        power_resource = Some(Resource::from(handle));
132                        continue;
133                    } else {
134                        continue;
135                    };
136                    startup_handles.push(StartupHandle { handle, info: HandleInfo::new(htype, 0) });
137                }
138                (ObjectType::VMO, Ok(name)) => {
139                    // Skip zero-sized VMOs.
140                    if handle.as_handle_ref().cast::<Vmo>().get_size() == Ok(0) {
141                        continue;
142                    }
143                    if name == "zbi" {
144                        zbi_vmo = Some(Vmo::from(handle));
145                    } else if name.as_bstr().starts_with(b"vdso/") {
146                        if vdso_vmo.is_none() {
147                            vdso_vmo =
148                                Some(Vmo::from(handle.duplicate_handle(Rights::SAME_RIGHTS)?));
149                        }
150                        startup_handles.push(StartupHandle {
151                            handle,
152                            info: HandleInfo::new(HandleType::VdsoVmo, vdso_count),
153                        });
154                        vdso_count += 1;
155                    } else {
156                        startup_handles.push(StartupHandle {
157                            handle,
158                            info: HandleInfo::new(HandleType::KernelFileVmo, kernel_file_count),
159                        });
160                        kernel_file_count += 1;
161                    }
162                }
163                (ObjectType::DEBUGLOG, _) => {
164                    log = Some(SystemLog::DebugLog(DebugLog::from(handle)));
165                }
166                (ObjectType::SOCKET, _) => {
167                    log = Some(SystemLog::Socket(Socket::from(handle)));
168                }
169                _ => {}
170            }
171        }
172
173        let root_job = root_job.ok_or_else(|| anyhow!("Root job handle not found"))?;
174        let vmex_resource =
175            vmex_resource.ok_or_else(|| anyhow!("VMEX resource handle not found"))?;
176        let vdso_vmo = vdso_vmo.ok_or_else(|| anyhow!("vDSO VMO handle not found"))?;
177        let zbi_vmo = zbi_vmo.ok_or_else(|| anyhow!("ZBI VMO handle not found"))?;
178        let log = log.ok_or_else(|| anyhow!("DebugLog/Socket handle not found"))?;
179
180        Ok(SystemHandles {
181            root_job,
182            vmex_resource,
183            power_resource,
184            vdso_vmo,
185            zbi_vmo,
186            log,
187            startup_handles,
188        })
189    }
190
191    /// Duplicates internal handles to create a new SystemHandles instance.
192    pub fn duplicate(&self) -> Result<Self, Status> {
193        Ok(Self {
194            root_job: self.root_job.duplicate_handle(Rights::SAME_RIGHTS)?,
195            vmex_resource: self.vmex_resource.duplicate_handle(Rights::SAME_RIGHTS)?,
196            power_resource: self
197                .power_resource
198                .as_ref()
199                .map(|p| p.duplicate_handle(Rights::SAME_RIGHTS))
200                .transpose()?,
201            vdso_vmo: self.vdso_vmo.duplicate_handle(Rights::SAME_RIGHTS)?,
202            zbi_vmo: self.zbi_vmo.duplicate_handle(Rights::SAME_RIGHTS)?,
203            log: self.log.duplicate(Rights::SAME_RIGHTS)?,
204            startup_handles: self
205                .startup_handles
206                .iter()
207                .map(|h| {
208                    Ok(StartupHandle {
209                        handle: h.handle.duplicate_handle(Rights::SAME_RIGHTS)?,
210                        info: h.info,
211                    })
212                })
213                .collect::<Result<Vec<_>, Status>>()?,
214        })
215    }
216}
217
218/// Reserves the low half of the address space so initial process allocations
219/// (program, vDSO, stack) stay out of low memory required by sanitizers like ASan.
220fn reserve_low_address_space(root_vmar: &Vmar) -> Result<Vmar, Error> {
221    let info = root_vmar.info()?;
222    let page_size = zx::system_get_page_size() as usize;
223    let reserve_len = (info.len / 2) & !(page_size - 1);
224    let (vmar, addr) = root_vmar.allocate(0, reserve_len, VmarFlags::SPECIFIC)?;
225    if addr != info.base {
226        bail!("zx_vmar_allocate gave wrong address for low address space reservation");
227    }
228    Ok(vmar)
229}
230
231/// Creates a child VMO representing a file entry within the BOOTFS image at the given
232/// `offset` and `size`, and marks it as executable using the VMEX resource.
233fn create_bootfs_file_vmo(
234    bootfs_vmo: &Vmo,
235    offset: u64,
236    size: u64,
237    vmex_resource: &Resource,
238) -> Result<Vmo, Error> {
239    bootfs_vmo
240        .create_child(VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE, offset, size)?
241        .replace_as_executable(vmex_resource)
242        .map_err(Into::into)
243}
244
245/// Loads and executes the target binary (and its ELF interpreter if dynamically linked)
246/// from the provided BOOTFS VMO into a new child process under `handles.root_job`.
247///
248/// If the program requires dynamic linking (`PT_INTERP`), this function also sets up and
249/// asynchronously serves the `fuchsia.ldsvc.Loader` service to resolve dynamic library
250/// dependencies.
251pub async fn launch_program(
252    program_name: &str,
253    target_path: &str,
254    args: impl IntoIterator<Item = &str>,
255    bootfs_vmo: Vmo,
256    handles: SystemHandles,
257    mut bootfs_entries: Option<&mut Vec<(u32, Vmo)>>,
258    log: &mut Log,
259) -> Result<Process, Error> {
260    let SystemHandles {
261        root_job,
262        vmex_resource,
263        power_resource: _,
264        vdso_vmo,
265        zbi_vmo,
266        log: system_log,
267        startup_handles,
268    } = handles;
269
270    let (child_process, child_vmar) =
271        root_job.create_child_process(ProcessOptions::empty(), program_name.as_bytes())?;
272    let reserved_vmar = reserve_low_address_space(&child_vmar)?;
273    let child_thread = child_process.create_thread(program_name.as_bytes())?;
274
275    let bootfs_parser =
276        BootfsParser::create_from_vmo(bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?)?;
277
278    let entry = bootfs_parser
279        .zero_copy_iter()
280        .filter_map(Result::ok)
281        .find(|e| e.name == target_path)
282        .ok_or_else(|| anyhow!("Program '{}' not found in bootfs", target_path))?;
283
284    let program_vmo =
285        create_bootfs_file_vmo(&bootfs_vmo, entry.offset, entry.size, &vmex_resource)?;
286    program_vmo.set_name(&Name::new_lossy(program_name))?;
287
288    if let Some(ref mut bootfs_entries) = bootfs_entries {
289        if let Ok(vmo_dup) = program_vmo.duplicate_handle(Rights::SAME_RIGHTS) {
290            bootfs_entries.push((entry.offset as u32, vmo_dup));
291        }
292    }
293
294    let headers = Elf64Headers::from_vmo(&program_vmo)?;
295
296    let (to_child_client, to_child_server) = Channel::create();
297    let mut loader_server = None;
298
299    let entry_point = if let Some(interp_hdr) =
300        headers.program_header_with_type(SegmentType::Interp)?
301    {
302        let mut interp_bytes = vec![0u8; interp_hdr.filesz as usize];
303        program_vmo.read(&mut interp_bytes, interp_hdr.offset as u64)?;
304        let interp_str = str::from_utf8(&interp_bytes)?.trim_end_matches('\0');
305        let interp_path = if interp_str.starts_with('/') {
306            interp_str.trim_start_matches('/')
307        } else if !interp_str.starts_with("lib/") {
308            &format!("lib/{}", interp_str)
309        } else {
310            interp_str
311        };
312
313        let interp_entry = bootfs_parser
314            .zero_copy_iter()
315            .filter_map(Result::ok)
316            .find(|e| e.name == interp_path || e.name.ends_with(&format!("/{}", interp_path)))
317            .ok_or_else(|| anyhow!("Interpreter '{}' not found in bootfs", interp_path))?;
318
319        let interp_vmo = create_bootfs_file_vmo(
320            &bootfs_vmo,
321            interp_entry.offset,
322            interp_entry.size,
323            &vmex_resource,
324        )?;
325        interp_vmo.set_name(&Name::new_lossy(&interp_path))?;
326
327        if let Some(ref mut bootfs_entries) = bootfs_entries {
328            if let Ok(vmo_dup) = interp_vmo.duplicate_handle(Rights::SAME_RIGHTS) {
329                if !bootfs_entries.iter().any(|(off, _)| *off == interp_entry.offset as u32) {
330                    bootfs_entries.push((interp_entry.offset as u32, vmo_dup));
331                }
332            }
333        }
334
335        let interp_headers = Elf64Headers::from_vmo(&interp_vmo)?;
336        let loaded_interp = load_elf(&interp_vmo, &interp_headers, &child_vmar)?;
337
338        let (ldsvc_client, ldsvc_server) = Channel::create();
339        loader_server = Some(ldsvc_server);
340
341        let handles = vec![
342            StartupHandle {
343                handle: program_vmo.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
344                info: HandleInfo::new(HandleType::ExecutableVmo, 0),
345            },
346            StartupHandle {
347                handle: system_log.to_handle(Rights::SAME_RIGHTS)?,
348                info: HandleInfo::new(HandleType::FileDescriptor, FDIO_FLAG_USE_FOR_STDIO),
349            },
350            StartupHandle {
351                handle: child_process.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
352                info: HandleInfo::new(HandleType::ProcessSelf, 0),
353            },
354            StartupHandle {
355                handle: child_vmar.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
356                info: HandleInfo::new(HandleType::RootVmar, 0),
357            },
358            StartupHandle {
359                handle: loaded_interp.vmar.into_handle().into(),
360                info: HandleInfo::new(HandleType::LoadedVmar, 0),
361            },
362            StartupHandle {
363                handle: child_thread.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
364                info: HandleInfo::new(HandleType::ThreadSelf, 0),
365            },
366            StartupHandle {
367                handle: ldsvc_client.into_handle().into(),
368                info: HandleInfo::new(HandleType::LdsvcLoader, 0),
369            },
370        ];
371
372        let contents = MessageContents {
373            // Passing LD_DEBUG=1 to the dynamic linker (PT_INTERP) enables verbose loading
374            // and relocation logs on the console for early system boot diagnostic purposes.
375            environment_vars: vec![CString::new("LD_DEBUG=1")?],
376            handles,
377            ..Default::default()
378        };
379
380        let message = Message::build(contents)?;
381        message.write(&to_child_client)?;
382        loaded_interp.entry
383    } else {
384        let loaded_elf = load_elf(&program_vmo, &headers, &child_vmar)?;
385        let mut initial_handles = vec![
386            system_log.to_handle(Rights::SAME_RIGHTS)?,
387            child_process.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle(),
388            child_vmar.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle(),
389            loaded_elf.vmar.into_handle(),
390            child_thread.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle(),
391        ];
392        to_child_client.write(&[], &mut initial_handles)?;
393        loaded_elf.entry
394    };
395
396    let vdso_headers = Elf64Headers::from_vmo(&vdso_vmo)?;
397    let loaded_vdso = load_elf(&vdso_vmo, &vdso_headers, &child_vmar)?;
398    let vdso_base = loaded_vdso.vmar_base;
399
400    let stack_size: usize = 256 * 1024;
401    let stack_vmo = Vmo::create(stack_size as u64)?;
402    stack_vmo.set_name(&Name::new_lossy("userboot-child-initial-stack"))?;
403    let stack_base = child_vmar.map(
404        0,
405        &stack_vmo,
406        0,
407        stack_size,
408        VmarFlags::PERM_READ | VmarFlags::PERM_WRITE,
409    )?;
410    let sp = compute_initial_stack_pointer(stack_base, stack_size);
411
412    let mut handles = vec![
413        StartupHandle {
414            handle: child_process.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
415            info: HandleInfo::new(HandleType::ProcessSelf, 0),
416        },
417        StartupHandle {
418            handle: child_vmar.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
419            info: HandleInfo::new(HandleType::RootVmar, 0),
420        },
421        StartupHandle {
422            handle: child_thread.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
423            info: HandleInfo::new(HandleType::ThreadSelf, 0),
424        },
425        StartupHandle {
426            handle: root_job.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
427            info: HandleInfo::new(HandleType::DefaultJob, 0),
428        },
429        StartupHandle {
430            handle: zbi_vmo.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
431            info: HandleInfo::new(HandleType::BootdataVmo, 0),
432        },
433        StartupHandle {
434            handle: bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?.into_handle().into(),
435            info: HandleInfo::new(HandleType::BootfsVmo, 0),
436        },
437        StartupHandle {
438            handle: system_log.to_handle(Rights::SAME_RIGHTS)?,
439            info: HandleInfo::new(HandleType::FileDescriptor, FDIO_FLAG_USE_FOR_STDIO),
440        },
441    ];
442    handles.extend(startup_handles);
443
444    let contents = MessageContents {
445        args: args.into_iter().map(CString::new).collect::<Result<Vec<_>, _>>()?,
446        handles,
447        namespace_paths: vec![CString::new("/svc")?],
448        ..Default::default()
449    };
450
451    let message = Message::build(contents)?;
452    message.write(&to_child_client)?;
453
454    // Destroy the low address space reservation VMAR now that all initial mappings (program ELF,
455    // vDSO, stack) have been placed in upper address space. This frees up the lower address space
456    // for sanitizers.
457    //
458    // SAFETY: `reserved_vmar` contains no active mappings and destroying it safely frees low
459    // address space for ASan.
460    unsafe {
461        reserved_vmar.destroy()?;
462    }
463
464    child_process.start(&child_thread, entry_point, sp, to_child_server.into(), vdso_base)?;
465    writeln!(log, "Started child process: {}", program_name)?;
466
467    if let Some(ldsvc_server) = loader_server {
468        writeln!(log, "Serving loader service for child process...")?;
469        let bootfs_parser =
470            BootfsParser::create_from_vmo(bootfs_vmo.duplicate_handle(Rights::SAME_RIGHTS)?)?;
471        let server = UserbootLoaderServer::new(
472            bootfs_parser,
473            bootfs_vmo,
474            vmex_resource,
475            bootfs_entries.is_some(),
476        );
477        if let Ok(Ok(server)) =
478            ServerEnd::<Loader, Channel>::from_untyped(ldsvc_server).spawn(server).await
479        {
480            if let (Some(bootfs_entries), Some(mut server_entries)) =
481                (bootfs_entries, server.entries)
482            {
483                bootfs_entries.append(&mut server_entries);
484            }
485        }
486    }
487
488    Ok(child_process)
489}
490
491/// Serves `fuchsia.ldsvc.Loader` for dynamic library loading during early boot.
492struct UserbootLoaderServer {
493    /// Bootfs parser used to locate dynamic libraries in the BOOTFS image.
494    bootfs_parser: BootfsParser,
495    /// VMO handle containing the BOOTFS filesystem image.
496    bootfs_vmo: Vmo,
497    /// System VMEX resource handle required to mark dynamic library VMOs as executable.
498    vmex_resource: Resource,
499    /// Optional vector for collected BOOTFS file VMO entries loaded via loader service.
500    entries: Option<Vec<(u32, Vmo)>>,
501    /// Configured subdirectory prefix for library resolution (e.g. "asan").
502    subdir: String,
503    /// Whether library lookup in fallback directory is excluded.
504    exclusive: bool,
505}
506
507impl UserbootLoaderServer {
508    fn new(
509        bootfs_parser: BootfsParser,
510        bootfs_vmo: Vmo,
511        vmex_resource: Resource,
512        collect_entries: bool,
513    ) -> Self {
514        Self {
515            bootfs_parser,
516            bootfs_vmo,
517            vmex_resource,
518            entries: collect_entries.then(Vec::new),
519            subdir: String::new(),
520            exclusive: false,
521        }
522    }
523}
524
525impl LoaderServerHandler<Channel> for UserbootLoaderServer {
526    /// Called when the loader client sends `Done`.
527    async fn done(&mut self) {}
528
529    /// Loads a shared library object from the `lib/` directory in BOOTFS.
530    async fn load_object(
531        &mut self,
532        request: Request<LoadObject, Channel>,
533        responder: Responder<LoadObject, Channel>,
534    ) {
535        let name = &request.payload().object_name;
536        let find_file = |path: &str| {
537            self.bootfs_parser
538                .zero_copy_iter()
539                .filter_map(Result::ok)
540                .find(|e| e.name == path || e.name.ends_with(&format!("/{path}")))
541        };
542
543        let mut found = None;
544        if !self.subdir.is_empty() {
545            found = find_file(&format!("lib/{}/{}", self.subdir, name));
546        }
547        if found.is_none() && (!self.exclusive || self.subdir.is_empty()) {
548            found = find_file(&format!("lib/{name}"));
549        }
550
551        let lib_vmo = found.as_ref().and_then(|entry| {
552            create_bootfs_file_vmo(&self.bootfs_vmo, entry.offset, entry.size, &self.vmex_resource)
553                .ok()
554        });
555
556        if let (Some(entries), Some(entry), Some(vmo)) = (&mut self.entries, &found, &lib_vmo) {
557            let offset = entry.offset as u32;
558            if !entries.iter().any(|(off, _)| *off == offset) {
559                if let Ok(vmo_dup) = vmo.duplicate_handle(Rights::SAME_RIGHTS) {
560                    entries.push((offset, vmo_dup));
561                }
562            }
563        }
564
565        let (rv, object) = lib_vmo.map_or((Status::NOT_FOUND, None), |v| (Status::OK, Some(v)));
566        let _ = responder.respond(LoaderLoadObjectResponse { rv, object }).await;
567    }
568
569    /// Configures the loader service search path prefix.
570    async fn config(
571        &mut self,
572        request: Request<Config, Channel>,
573        responder: Responder<Config, Channel>,
574    ) {
575        let payload = request.payload();
576        let config_str = payload.config.as_str();
577        self.exclusive = config_str.ends_with('!');
578        self.subdir = config_str.strip_suffix('!').unwrap_or(config_str).to_string();
579        let _ = responder.respond(Status::OK).await;
580    }
581
582    /// Clones the loader service handle (unsupported in userboot).
583    async fn clone(
584        &mut self,
585        _request: Request<Clone, Channel>,
586        responder: Responder<Clone, Channel>,
587    ) {
588        let _ = responder.respond(Status::NOT_SUPPORTED).await;
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use elf_parse::{
596        CURRENT_ARCH, ELF_MAGIC, Elf64FileHeader, Elf64ProgramHeader, ElfClass, ElfIdent, ElfType,
597        ElfVersion, NATIVE_ENCODING, SegmentFlags,
598    };
599    use fidl_fuchsia_kernel::{PowerResourceMarker, VmexResourceMarker};
600    use fuchsia_bootfs::bootfs::ZBI_BOOTFS_MAGIC;
601    use fuchsia_component::client::connect_to_protocol;
602    use fuchsia_runtime::{job_default, take_startup_handle};
603    use std::mem::size_of;
604    use std::sync::LazyLock;
605    use zerocopy::IntoBytes;
606
607    static VDSO_VMO: LazyLock<Vmo> = LazyLock::new(|| {
608        take_startup_handle(HandleInfo::new(HandleType::VdsoVmo, 0))
609            .map(Vmo::from)
610            .expect("expected real vDSO VMO handle from process startup handles")
611    });
612
613    async fn create_test_system_handles() -> SystemHandles {
614        let root_job = job_default().duplicate_handle(Rights::SAME_RIGHTS).unwrap();
615        let vmex_resource = connect_to_protocol::<VmexResourceMarker>()
616            .expect("failed to connect to VmexResource protocol")
617            .get()
618            .await
619            .expect("failed to get VmexResource");
620        let power_resource = connect_to_protocol::<PowerResourceMarker>()
621            .expect("failed to connect to PowerResource protocol")
622            .get()
623            .await
624            .expect("failed to get PowerResource");
625        let vdso_vmo = VDSO_VMO.duplicate_handle(Rights::SAME_RIGHTS).unwrap();
626        let zbi_vmo = Vmo::create(4096).unwrap();
627        let (s1, _s2) = zx::Socket::create_stream();
628        let log = SystemLog::Socket(s1);
629        let startup_handles = vec![];
630
631        SystemHandles {
632            root_job,
633            vmex_resource,
634            power_resource: Some(power_resource),
635            vdso_vmo,
636            zbi_vmo,
637            log,
638            startup_handles,
639        }
640    }
641
642    fn create_bootfs_vmo(files: &[(&str, &[u8])]) -> Vmo {
643        let mut total_dirsize = 0u32;
644        for (name, _) in files {
645            let name_len = (name.len() + 1) as u32;
646            let dirent_size = (12 + name_len + 3) & !3;
647            total_dirsize += dirent_size;
648        }
649
650        let data_start_offset = ((16 + total_dirsize + 4095) & !4095) as u64;
651        let mut vmo_bytes = vec![0u8; data_start_offset as usize];
652
653        // Write header
654        vmo_bytes[0..4].copy_from_slice(&ZBI_BOOTFS_MAGIC.to_le_bytes());
655        vmo_bytes[4..8].copy_from_slice(&total_dirsize.to_le_bytes());
656
657        let mut current_dir_off = 16usize;
658        let mut current_data_off = data_start_offset;
659
660        for (name, data) in files {
661            let name_c = format!("{}\0", name);
662            let name_bytes = name_c.as_bytes();
663            let name_len = name_bytes.len() as u32;
664            let data_len = data.len() as u32;
665
666            vmo_bytes[current_dir_off..current_dir_off + 4]
667                .copy_from_slice(&name_len.to_le_bytes());
668            vmo_bytes[current_dir_off + 4..current_dir_off + 8]
669                .copy_from_slice(&data_len.to_le_bytes());
670            vmo_bytes[current_dir_off + 8..current_dir_off + 12]
671                .copy_from_slice(&(current_data_off as u32).to_le_bytes());
672            vmo_bytes[current_dir_off + 12..current_dir_off + 12 + name_bytes.len()]
673                .copy_from_slice(name_bytes);
674
675            let dirent_size = ((12 + name_len + 3) & !3) as usize;
676            current_dir_off += dirent_size;
677
678            if vmo_bytes.len() < (current_data_off as usize + data.len()) {
679                vmo_bytes.resize(current_data_off as usize + data.len(), 0);
680            }
681            vmo_bytes[current_data_off as usize..current_data_off as usize + data.len()]
682                .copy_from_slice(data);
683
684            let page_aligned_data_len = ((data.len() + 4095) & !4095) as u64;
685            current_data_off += page_aligned_data_len.max(4096);
686        }
687
688        let vmo = Vmo::create(vmo_bytes.len() as u64).unwrap();
689        vmo.write(&vmo_bytes, 0).unwrap();
690        vmo
691    }
692
693    /// Generates a raw ELF64 buffer in memory with an optional PT_INTERP segment.
694    fn create_elf_bytes(interp_path: Option<&str>) -> Vec<u8> {
695        let has_interp = interp_path.is_some();
696        let phnum = if has_interp { 2 } else { 1 };
697        let ehsize = size_of::<Elf64FileHeader>() as u16;
698        let phentsize = size_of::<Elf64ProgramHeader>() as u16;
699
700        let interp_str_bytes = interp_path.map(|p| format!("{}\0", p)).unwrap_or_default();
701        let interp_len = interp_str_bytes.len();
702
703        let phoff = ehsize as usize;
704        let interp_offset = phoff + (phnum as usize) * (phentsize as usize);
705
706        let file_header = Elf64FileHeader {
707            ident: ElfIdent {
708                magic: ELF_MAGIC,
709                class: ElfClass::Elf64 as u8,
710                data: NATIVE_ENCODING as u8,
711                version: ElfVersion::Current as u8,
712                osabi: 0,
713                abiversion: 0,
714                pad: [0; 7],
715            },
716            elf_type: ElfType::Executable as u16,
717            machine: CURRENT_ARCH as u16,
718            version: 1,
719            entry: 0x1000,
720            phoff,
721            shoff: 0,
722            flags: 0,
723            ehsize,
724            phentsize,
725            phnum,
726            shentsize: 0,
727            shnum: 0,
728            shstrndx: 0,
729        };
730
731        let mut bytes = Vec::new();
732        bytes.extend_from_slice(file_header.as_bytes());
733
734        // PT_LOAD segment header
735        let load_phdr = Elf64ProgramHeader {
736            segment_type: SegmentType::Load as u32,
737            flags: (SegmentFlags::READ | SegmentFlags::EXECUTE).bits(),
738            offset: 0,
739            vaddr: 0x1000,
740            paddr: 0x1000,
741            filesz: 4096,
742            memsz: 4096,
743            align: 4096,
744        };
745        bytes.extend_from_slice(load_phdr.as_bytes());
746
747        if let Some(_) = interp_path {
748            let interp_phdr = Elf64ProgramHeader {
749                segment_type: SegmentType::Interp as u32,
750                flags: SegmentFlags::READ.bits(),
751                offset: interp_offset,
752                vaddr: 0,
753                paddr: 0,
754                filesz: interp_len as u64,
755                memsz: interp_len as u64,
756                align: 1,
757            };
758            bytes.extend_from_slice(interp_phdr.as_bytes());
759            bytes.extend_from_slice(interp_str_bytes.as_bytes());
760        }
761
762        if bytes.len() < 4096 {
763            bytes.resize(4096, 0);
764        }
765
766        bytes
767    }
768
769    #[fuchsia::test]
770    async fn test_launch_program_invalid_bootfs() {
771        let handles = create_test_system_handles().await;
772        let invalid_bootfs = Vmo::create(4096).unwrap();
773        let mut log = Log::new();
774
775        let res = launch_program(
776            "test_prog",
777            "bin/test_prog",
778            ["arg1"],
779            invalid_bootfs,
780            handles,
781            None,
782            &mut log,
783        )
784        .await;
785
786        assert!(res.is_err());
787    }
788
789    #[fuchsia::test]
790    async fn test_launch_program_target_not_in_bootfs() {
791        let handles = create_test_system_handles().await;
792        let bootfs = create_bootfs_vmo(&[("bin/other", b"some_data")]);
793        let mut log = Log::new();
794
795        let res =
796            launch_program("test_prog", "bin/test_prog", ["arg1"], bootfs, handles, None, &mut log)
797                .await;
798
799        assert!(res.is_err());
800        let err_msg = res.unwrap_err().to_string();
801        assert!(
802            err_msg.contains("Program 'bin/test_prog' not found in bootfs"),
803            "unexpected err: {err_msg}"
804        );
805    }
806
807    #[fuchsia::test]
808    async fn test_launch_program_corrupt_elf() {
809        let handles = create_test_system_handles().await;
810        let bootfs = create_bootfs_vmo(&[("bin/test_prog", b"NOT_AN_ELF_BINARY")]);
811        let mut log = Log::new();
812
813        let res =
814            launch_program("test_prog", "bin/test_prog", [], bootfs, handles, None, &mut log).await;
815
816        assert!(res.is_err());
817    }
818
819    #[fuchsia::test]
820    async fn test_launch_program_missing_interpreter() {
821        let handles = create_test_system_handles().await;
822        let elf_data = create_elf_bytes(Some("lib/ld.so.1"));
823        let bootfs = create_bootfs_vmo(&[("bin/test_prog", &elf_data)]);
824        let mut log = Log::new();
825
826        let res = launch_program(
827            "test_prog",
828            "bin/test_prog",
829            ["--flag"],
830            bootfs,
831            handles,
832            None,
833            &mut log,
834        )
835        .await;
836
837        assert!(res.is_err());
838        let err_msg = res.unwrap_err().to_string();
839        assert!(
840            err_msg.contains("Interpreter 'lib/ld.so.1' not found in bootfs"),
841            "unexpected err: {err_msg}"
842        );
843    }
844
845    #[fuchsia::test]
846    async fn test_launch_program_interpreter_path_normalization() {
847        let handles = create_test_system_handles().await;
848        // Leading slash /lib/ld.so.1 should normalize to lib/ld.so.1
849        let elf_data = create_elf_bytes(Some("/lib/ld.so.1"));
850        let bootfs = create_bootfs_vmo(&[("bin/test_prog", &elf_data)]);
851        let mut log = Log::new();
852
853        let res =
854            launch_program("test_prog", "bin/test_prog", [], bootfs, handles, None, &mut log).await;
855
856        assert!(res.is_err());
857        let err_msg = res.unwrap_err().to_string();
858        assert!(
859            err_msg.contains("Interpreter 'lib/ld.so.1' not found in bootfs"),
860            "unexpected err: {err_msg}"
861        );
862
863        // Naked filename "ld.so.1" should prefix with "lib/" -> "lib/ld.so.1"
864        let handles2 = create_test_system_handles().await;
865        let elf_data2 = create_elf_bytes(Some("ld.so.1"));
866        let bootfs2 = create_bootfs_vmo(&[("bin/test_prog", &elf_data2)]);
867        let mut log2 = Log::new();
868
869        let res2 =
870            launch_program("test_prog", "bin/test_prog", [], bootfs2, handles2, None, &mut log2)
871                .await;
872
873        assert!(res2.is_err());
874        let err_msg2 = res2.unwrap_err().to_string();
875        assert!(
876            err_msg2.contains("Interpreter 'lib/ld.so.1' not found in bootfs"),
877            "unexpected err: {err_msg2}"
878        );
879    }
880
881    #[fuchsia::test]
882    async fn test_launch_program_success() {
883        let handles = create_test_system_handles().await;
884        let elf_data = create_elf_bytes(None);
885        let bootfs = create_bootfs_vmo(&[("bin/test_prog", &elf_data)]);
886        let mut log = Log::new();
887
888        let mut bootfs_entries = Vec::new();
889        let res = launch_program(
890            "test_prog",
891            "bin/test_prog",
892            ["arg1", "arg2"],
893            bootfs,
894            handles,
895            Some(&mut bootfs_entries),
896            &mut log,
897        )
898        .await;
899
900        assert!(res.is_ok(), "expected launch_program to succeed, got: {:?}", res);
901        assert!(!bootfs_entries.is_empty());
902    }
903
904    #[fuchsia::test]
905    async fn test_launch_program_arg_with_null_byte() {
906        let handles = create_test_system_handles().await;
907        let elf_data = create_elf_bytes(None);
908        let bootfs = create_bootfs_vmo(&[("bin/test_prog", &elf_data)]);
909        let mut log = Log::new();
910
911        let res = launch_program(
912            "test_prog",
913            "bin/test_prog",
914            ["arg1\0with_null"],
915            bootfs,
916            handles,
917            None,
918            &mut log,
919        )
920        .await;
921
922        assert!(res.is_err());
923    }
924
925    #[fuchsia::test]
926    async fn test_system_handles_success() {
927        let root_job = job_default().duplicate_handle(Rights::SAME_RIGHTS).unwrap();
928        root_job.set_name(&Name::new("root").unwrap()).unwrap();
929        let root_job_info =
930            zx::HandleInfo::new(root_job.into_handle(), ObjectType::JOB, Rights::SAME_RIGHTS);
931
932        let vmex = connect_to_protocol::<VmexResourceMarker>().unwrap().get().await.unwrap();
933        let vmex_info =
934            zx::HandleInfo::new(vmex.into_handle(), ObjectType::RESOURCE, Rights::SAME_RIGHTS);
935
936        let power = connect_to_protocol::<PowerResourceMarker>().unwrap().get().await.unwrap();
937        let power_info =
938            zx::HandleInfo::new(power.into_handle(), ObjectType::RESOURCE, Rights::SAME_RIGHTS);
939
940        let vdso = VDSO_VMO.duplicate_handle(Rights::SAME_RIGHTS).unwrap();
941        vdso.set_name(&Name::new("vdso/stable").unwrap()).unwrap();
942        let vdso_info =
943            zx::HandleInfo::new(vdso.into_handle(), ObjectType::VMO, Rights::SAME_RIGHTS);
944
945        let zbi = Vmo::create(4096).unwrap();
946        zbi.set_name(&Name::new("zbi").unwrap()).unwrap();
947        let zbi_info = zx::HandleInfo::new(zbi.into_handle(), ObjectType::VMO, Rights::SAME_RIGHTS);
948
949        let (s1, _s2) = zx::Socket::create_stream();
950        let log_info =
951            zx::HandleInfo::new(s1.into_handle(), ObjectType::SOCKET, Rights::SAME_RIGHTS);
952
953        let zero_vmo = Vmo::create(0).unwrap();
954        zero_vmo.set_name(&Name::new("zero").unwrap()).unwrap();
955        let zero_info =
956            zx::HandleInfo::new(zero_vmo.into_handle(), ObjectType::VMO, Rights::SAME_RIGHTS);
957
958        let kfile_vmo = Vmo::create(4096).unwrap();
959        kfile_vmo.set_name(&Name::new("kernel_file").unwrap()).unwrap();
960        let kfile_info =
961            zx::HandleInfo::new(kfile_vmo.into_handle(), ObjectType::VMO, Rights::SAME_RIGHTS);
962
963        let res = SystemHandles::from_handles(vec![
964            root_job_info,
965            vmex_info,
966            power_info,
967            vdso_info,
968            zbi_info,
969            log_info,
970            zero_info,
971            kfile_info,
972        ]);
973        let handles = res.expect("expected SystemHandles::from_handles to succeed");
974        assert_eq!(handles.root_job.get_name().unwrap(), "root");
975        assert_eq!(handles.vdso_vmo.get_name().unwrap(), "vdso/stable");
976        assert_eq!(handles.zbi_vmo.get_name().unwrap(), "zbi");
977        assert_eq!(handles.startup_handles.len(), 2);
978    }
979}