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