Skip to main content

starnix_kernel_runner/
component_runner.rs

1// Copyright 2022 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 crate::{MountAction, run_component_features};
6use anyhow::{Context, Error, anyhow, bail};
7use fidl::AsyncChannel;
8use fidl::endpoints::{ControlHandle, RequestStream, ServerEnd};
9use fidl_fuchsia_component as fcomponent;
10use fidl_fuchsia_component_runner::{
11    ComponentControllerMarker, ComponentControllerRequest, ComponentControllerRequestStream,
12    ComponentStartInfo,
13};
14use fidl_fuchsia_io as fio;
15use fidl_fuchsia_process as fprocess;
16use fuchsia_async as fasync;
17use fuchsia_runtime::{HandleInfo, HandleType};
18use futures::channel::oneshot;
19use futures::{FutureExt, StreamExt};
20use rand::distr::Alphanumeric;
21use rand::{Rng, rng};
22use serde::Deserialize;
23use serde::de::Error as _;
24use starnix_core::execution::{create_init_child_process, execute_task_with_prerun_result};
25use starnix_core::fs::fuchsia::{RemoteFs, SyslogFile, create_file_from_handle};
26use starnix_core::task::{CurrentTask, ExitStatus, Task};
27use starnix_core::vfs::fs_args::MountParams;
28use starnix_core::vfs::{
29    FdNumber, FdTable, FileSystemOptions, FsString, LookupContext, NamespaceNode, WhatToMount,
30};
31use starnix_core::{security, signals};
32use starnix_logging::{log_debug, log_error, log_info, log_warn};
33use starnix_sync::{
34    ComponentMountRecordLock, FileOpsCore, LockDepMutex, LockEqualOrBefore, Locked, Unlocked,
35};
36use starnix_task_command::TaskCommand;
37use starnix_uapi::auth::{Capabilities, Credentials};
38use starnix_uapi::device_id::DeviceId;
39use starnix_uapi::errno;
40use starnix_uapi::errors::{EEXIST, ENOTDIR, Errno};
41use starnix_uapi::file_mode::mode;
42use starnix_uapi::mount_flags::{MountFlags, MountpointFlags};
43use starnix_uapi::open_flags::OpenFlags;
44use starnix_uapi::signals::{SIGINT, SIGKILL};
45use starnix_uapi::unmount_flags::UnmountFlags;
46use std::ffi::CString;
47use std::ops::DerefMut;
48use std::os::unix::ffi::OsStrExt;
49use std::path::Path;
50use std::sync::{Arc, Weak};
51
52/// Component controller epitaph value used as the base value to pass non-zero error
53/// codes to the calling component.
54///
55/// TODO(https://fxbug.dev/42081234): Cleanup this once we have a proper mechanism to
56/// get Linux exit code from component runner.
57const COMPONENT_EXIT_CODE_BASE: i32 = 1024;
58
59#[derive(Debug, Deserialize)]
60#[serde(deny_unknown_fields)]
61struct ComponentProgram {
62    binary: CString,
63
64    #[serde(default)]
65    args: Vec<String>,
66
67    #[serde(default)]
68    environ: Vec<String>,
69
70    #[serde(default)]
71    cwd: Option<String>,
72
73    #[serde(default)]
74    uid: Option<runner::serde::StoreAsString<u32>>,
75
76    #[serde(default)]
77    component_mounts: Vec<String>,
78
79    #[serde(default)]
80    features: Vec<String>,
81
82    #[serde(default, deserialize_with = "parse_capabilities")]
83    capabilities: Option<Capabilities>,
84
85    #[serde(default)]
86    seclabel: Option<CString>,
87
88    #[serde(default, rename(deserialize = "test_target_kernel"))]
89    _test_target_kernel: Option<String>,
90}
91
92impl ComponentProgram {
93    fn resolve_templates(&mut self, component_path: &str, pkg_path: &str) {
94        let resolve_template = |values: &mut Vec<String>| {
95            for val in values {
96                *val = val
97                    .replace("{pkg_path}", &pkg_path)
98                    .replace("{component_path}", &component_path);
99            }
100        };
101
102        resolve_template(&mut self.args);
103        resolve_template(&mut self.environ);
104    }
105}
106
107fn parse_capabilities<'de, D>(deserializer: D) -> Result<Option<Capabilities>, D::Error>
108where
109    D: serde::Deserializer<'de>,
110{
111    let mut capabilities = Capabilities::empty();
112    for cap in Vec::<String>::deserialize(deserializer)? {
113        capabilities |= cap.parse().map_err(D::Error::custom)?;
114    }
115    Ok(Some(capabilities))
116}
117
118/// Starts a component inside the given container.
119///
120/// The component's `binary` can either:
121///   - an absolute path, in which case the path is treated as a path into the root filesystem that
122///     is mounted by the container's configuration
123///   - relative path, in which case the binary is read from the component's package (which is
124///     mounted at /container/component/{random}/pkg.)
125///
126/// The directories in the component's namespace are mounted at /container/component/{random}.
127pub async fn start_component(
128    mut start_info: ComponentStartInfo,
129    controller: ServerEnd<ComponentControllerMarker>,
130    system_task: &CurrentTask,
131) -> Result<(), Error> {
132    let url = start_info.resolved_url.clone().unwrap_or_else(|| "<unknown>".to_string());
133
134    let (task_complete_sender, task_complete) = oneshot::channel::<TaskResult>();
135
136    let weak_task = system_task.override_creds(
137        security::creds_start_internal_operation(system_task),
138        || {
139            // TODO(https://fxbug.dev/42076551): We leak the directory created by this function.
140            let component_path = generate_component_path(
141                system_task.kernel().kthreads.unlocked_for_async().deref_mut(),
142                system_task,
143            )?;
144            let pkg_path = format!("{component_path}/pkg");
145
146            let mount_record =
147                Arc::new(LockDepMutex::<_, ComponentMountRecordLock>::new(MountRecord::default()));
148
149            let ns = start_info.ns.take().ok_or_else(|| anyhow!("Missing namespace"))?;
150
151            let program = start_info.program.as_ref().context("reading program block")?;
152            let mut program: ComponentProgram =
153                runner::serde::deserialize_program(program).context("parsing program block")?;
154            program.resolve_templates(&component_path, &pkg_path);
155            log_debug!("start_component: {}\n{:#?}", url, program);
156
157            let ns_mount_options = system_task.kernel().features.default_ns_mount_options.as_ref();
158            let mut maybe_pkg = None;
159            let mut maybe_svc = None;
160            for entry in ns {
161                if let (Some(dir_path), Some(dir_handle)) = (entry.path, entry.directory) {
162                    let dir_path_str = dir_path.as_str();
163                    let mount_options = ns_mount_options
164                        .and_then(|mount_options| mount_options.get(dir_path_str).cloned());
165
166                    match dir_path_str {
167                        "/svc" => {
168                            maybe_svc = Some(fio::DirectoryProxy::new(AsyncChannel::from_channel(
169                                dir_handle.into_channel(),
170                            )));
171                        }
172                        "/custom_artifacts" => {
173                            // Mount custom_artifacts directory at root of container
174                            // We may want to transition to have this directory unique per component
175                            let dir_proxy =
176                                fio::DirectorySynchronousProxy::new(dir_handle.into_channel());
177                            mount_record
178                                .lock()
179                                .mount_remote(
180                                    system_task.kernel().kthreads.unlocked_for_async().deref_mut(),
181                                    system_task,
182                                    &dir_proxy,
183                                    &dir_path,
184                                    mount_options.as_ref(),
185                                )
186                                .with_context(|| {
187                                    format!("failed to mount_remote on path {}", dir_path)
188                                })?;
189                        }
190                        _ => {
191                            let dir_proxy =
192                                fio::DirectorySynchronousProxy::new(dir_handle.into_channel());
193                            mount_record
194                                .lock()
195                                .mount_remote(
196                                    system_task.kernel().kthreads.unlocked_for_async().deref_mut(),
197                                    system_task,
198                                    &dir_proxy,
199                                    &format!("{component_path}/{dir_path}"),
200                                    mount_options.as_ref(),
201                                )
202                                .with_context(|| {
203                                    format!(
204                                        "failed to mount_remote on path {component_path}/{dir_path}"
205                                    )
206                                })?;
207                            if dir_path == "/pkg" {
208                                maybe_pkg = Some(dir_proxy);
209                            }
210                        }
211                    }
212                }
213            }
214
215            let pkg = maybe_pkg.ok_or_else(|| anyhow!("Missing /pkg entry in namespace"))?;
216
217            let uid = program
218                .uid
219                .map(|uid| uid.0)
220                .unwrap_or_else(|| system_task.kernel().features.default_uid);
221
222            let mut credentials = Credentials::with_ids(uid, uid);
223            if let Some(capabilities) = program.capabilities {
224                credentials.cap_permitted = capabilities;
225                credentials.cap_effective = capabilities;
226                credentials.cap_inheritable = capabilities;
227                credentials.cap_ambient = capabilities;
228            }
229
230            run_component_features(system_task.kernel(), &program.features, maybe_svc)
231                .unwrap_or_else(|e| {
232                    log_error!("failed to set component features for {} - {:?}", url, e);
233                });
234
235            let current_task = create_init_child_process(
236                system_task.kernel().kthreads.unlocked_for_async().deref_mut(),
237                system_task.kernel(),
238                TaskCommand::new(program.binary.as_bytes()),
239                credentials,
240                program.seclabel.as_ref(),
241            )?;
242
243            execute_task_with_prerun_result(
244                system_task.kernel().kthreads.unlocked_for_async().deref_mut(),
245                current_task,
246                {
247                    let mount_record = mount_record.clone();
248                    move |locked, current_task| {
249                        let cwd_path = FsString::from(program.cwd.unwrap_or(pkg_path));
250                        let cwd = current_task.lookup_path(
251                            locked,
252                            &mut LookupContext::default(),
253                            current_task.fs().root(),
254                            cwd_path.as_ref(),
255                        )?;
256                        current_task.fs().chdir(locked, current_task, cwd)?;
257
258                        for mount in &program.component_mounts {
259                            let action = MountAction::from_spec(locked, current_task, &pkg, mount)
260                                .map_err(|e| {
261                                    log_error!("Error while mounting the filesystems: {e:?}");
262                                    errno!(EINVAL)
263                                })?;
264                            let mount_point =
265                                current_task.lookup_path_from_root(locked, action.path.as_ref())?;
266                            mount_record.lock().mount(
267                                mount_point,
268                                WhatToMount::Fs(action.fs),
269                                action.flags,
270                            )?;
271                        }
272
273                        let files = current_task.files();
274                        parse_numbered_handles(
275                            locked,
276                            current_task,
277                            start_info.numbered_handles,
278                            &files,
279                        )
280                        .map_err(|e| {
281                            log_error!("Error while parsing the numbered handles: {e:?}");
282                            errno!(EINVAL)
283                        })?;
284
285                        let mut argv = vec![program.binary.clone()];
286                        for arg in program.args {
287                            argv.push(CString::new(arg).map_err(|_| errno!(EINVAL))?);
288                        }
289
290                        let mut environ = vec![];
291                        for env in program.environ {
292                            environ.push(CString::new(env).map_err(|_| errno!(EINVAL))?);
293                        }
294
295                        let executable = current_task.open_file(
296                            locked,
297                            program.binary.as_bytes().into(),
298                            OpenFlags::RDONLY,
299                        )?;
300                        current_task.exec(locked, executable, program.binary, argv, environ)?;
301
302                        Ok(Arc::downgrade(&current_task.task))
303                    }
304                },
305                move |result| {
306                    // Unmount all the directories for this component.
307                    std::mem::drop(mount_record);
308
309                    // If the component controller server has gone away, there is nobody for us to
310                    // report the result to.
311                    let _ = task_complete_sender.send(result);
312                },
313                None,
314            )
315            .map_err(anyhow::Error::from)
316        },
317    )?;
318
319    let controller = controller.into_stream();
320    fasync::Task::local(serve_component_controller(controller, weak_task, task_complete)).detach();
321
322    Ok(())
323}
324
325type TaskResult = Result<ExitStatus, Error>;
326
327/// Translates [ComponentControllerRequest] messages to signals on the `task`.
328///
329/// When a `Stop` request is received, it will send a `SIGINT` to the task.
330/// When a `Kill` request is received, it will send a `SIGKILL` to the task and close the component
331/// controller channel regardless if/how the task responded to the signal. Due to Linux's design,
332/// this may not reliably cleanup everything that was started as a result of running the component.
333///
334/// If the task has completed, it will also close the controller channel.
335async fn serve_component_controller(
336    controller: ComponentControllerRequestStream,
337    task: Weak<Task>,
338    task_complete: oneshot::Receiver<TaskResult>,
339) {
340    let controller_handle = controller.control_handle();
341
342    enum Event<T, U> {
343        Controller(T),
344        Completion(U),
345    }
346
347    let mut stream = futures::stream::select(
348        controller.map(Event::Controller),
349        task_complete.into_stream().map(Event::Completion),
350    );
351
352    while let Some(event) = stream.next().await {
353        match event {
354            Event::Controller(request) => match request {
355                Ok(ComponentControllerRequest::Stop { .. }) => {
356                    if let Some(task) = task.upgrade() {
357                        signals::send_standard_signal(
358                            task.kernel().kthreads.unlocked_for_async().deref_mut(),
359                            task.as_ref(),
360                            signals::SignalInfo::kernel(SIGINT),
361                        );
362                        log_info!("Sent SIGINT to program {}", task.command());
363                    }
364                }
365                Ok(ComponentControllerRequest::Kill { .. }) => {
366                    if let Some(task) = task.upgrade() {
367                        signals::send_standard_signal(
368                            task.kernel().kthreads.unlocked_for_async().deref_mut(),
369                            &task,
370                            signals::SignalInfo::kernel(SIGKILL),
371                        );
372                        log_info!("Sent SIGKILL to program {}", task.command());
373                        controller_handle.shutdown_with_epitaph(zx::Status::from_raw(
374                            fcomponent::Error::InstanceDied.into_primitive() as i32,
375                        ));
376                    }
377                    return;
378                }
379                Ok(ComponentControllerRequest::_UnknownMethod { ordinal, .. }) => {
380                    log_warn!("Unknown ComponentController request: {ordinal}");
381                }
382                Err(_) => {
383                    return;
384                }
385            },
386            Event::Completion(result) => match result {
387                Ok(Ok(ExitStatus::Exit(0))) => {
388                    controller_handle.shutdown_with_epitaph(zx::Status::OK)
389                }
390                Ok(Ok(ExitStatus::Exit(n))) => controller_handle.shutdown_with_epitaph(
391                    zx::Status::from_raw(COMPONENT_EXIT_CODE_BASE + n as i32),
392                ),
393                _ => controller_handle.shutdown_with_epitaph(zx::Status::from_raw(
394                    fcomponent::Error::InstanceDied.into_primitive() as i32,
395                )),
396            },
397        }
398    }
399}
400
401/// Returns /container/component/{random} that doesn't already exist
402fn generate_component_path<L>(
403    locked: &mut Locked<L>,
404    system_task: &CurrentTask,
405) -> Result<String, Error>
406where
407    L: LockEqualOrBefore<FileOpsCore>,
408{
409    // Checking container directory already exists.
410    // If this lookup fails, the container might not have the "container" feature enabled.
411    let mount_point = system_task.lookup_path_from_root(locked, "/container/component/".into())?;
412
413    // Find /container/component/{random} that doesn't already exist
414    let component_path = loop {
415        let random_string: String =
416            rng().sample_iter(&Alphanumeric).take(10).map(char::from).collect();
417
418        // This returns EEXIST if /container/component/{random} already exists.
419        // If so, try again with another {random} string.
420        match mount_point.create_node(
421            locked,
422            system_task,
423            random_string.as_str().into(),
424            mode!(IFDIR, 0o755),
425            DeviceId::NONE,
426        ) {
427            Ok(_) => break format!("/container/component/{random_string}"),
428            Err(errno) if errno == EEXIST => {}
429            Err(e) => bail!(e),
430        };
431    };
432
433    Ok(component_path)
434}
435
436/// Adds the given startup handles to a CurrentTask.
437///
438/// The `numbered_handles` of type `HandleType::FileDescriptor` are used to
439/// create files, and the handles are required to be of type `zx::Socket`.
440///
441/// If there is a `numbered_handles` of type `HandleType::User0`, that is
442/// interpreted as the server end of the ShellController protocol.
443pub fn parse_numbered_handles(
444    locked: &mut Locked<Unlocked>,
445    current_task: &CurrentTask,
446    numbered_handles: Option<Vec<fprocess::HandleInfo>>,
447    files: &FdTable,
448) -> Result<(), Error> {
449    if let Some(numbered_handles) = numbered_handles {
450        for numbered_handle in numbered_handles {
451            let info = HandleInfo::try_from(numbered_handle.id)?;
452            if info.handle_type() == HandleType::FileDescriptor {
453                let file = create_file_from_handle(locked, current_task, numbered_handle.handle)?;
454                files.insert(locked, current_task, FdNumber::from_raw(info.arg().into()), file)?;
455            }
456        }
457    }
458
459    let stdio = SyslogFile::new_file(locked, current_task);
460    // If no numbered handle is provided for each stdio handle, default to syslog.
461    for i in [0, 1, 2] {
462        if files.get(FdNumber::from_raw(i)).is_err() {
463            files.insert(locked, current_task, FdNumber::from_raw(i), stdio.clone())?;
464        }
465    }
466
467    Ok(())
468}
469
470/// A record of the mounts created when starting a component.
471///
472/// When the record is dropped, the mounts are unmounted.
473#[derive(Default)]
474struct MountRecord {
475    /// The namespace nodes at which we have crated mounts for this component.
476    mounts: Vec<NamespaceNode>,
477}
478
479impl MountRecord {
480    fn mount(
481        &mut self,
482        mount_point: NamespaceNode,
483        what: WhatToMount,
484        flags: MountpointFlags,
485    ) -> Result<(), Errno> {
486        mount_point.mount(what, flags.into())?;
487        self.mounts.push(mount_point);
488        Ok(())
489    }
490
491    fn mount_remote<L>(
492        &mut self,
493        locked: &mut Locked<L>,
494        system_task: &CurrentTask,
495        directory: &fio::DirectorySynchronousProxy,
496        path: &str,
497        mount_options: Option<&String>,
498    ) -> Result<(), Error>
499    where
500        L: LockEqualOrBefore<FileOpsCore>,
501    {
502        // The incoming dir_path might not be top level, e.g. it could be /foo/bar.
503        // Iterate through each component directory starting from the parent and
504        // create it if it doesn't exist.
505        let mut current_node =
506            system_task.lookup_path_from_root(locked, ".".into()).context("looking up '.'")?;
507        let mut context = LookupContext::default();
508
509        // Extract each component using Path::new(path).components(). For example,
510        // Path::new("/foo/bar").components() will return [RootDir, Normal("foo"), Normal("bar")].
511        // We're not interested in the RootDir, so we drop the prefix "/" if it exists.
512        let path = if let Some(path) = path.strip_prefix('/') { path } else { path };
513
514        for sub_dir in Path::new(path).components() {
515            let sub_dir_bytes = sub_dir.as_os_str().as_bytes();
516            current_node = match current_node.create_node(
517                locked,
518                system_task,
519                sub_dir_bytes.into(),
520                mode!(IFDIR, 0o755),
521                DeviceId::NONE,
522            ) {
523                Ok(node) => node,
524                Err(errno) if errno == EEXIST || errno == ENOTDIR => current_node
525                    .lookup_child(locked, system_task, &mut context, sub_dir_bytes.into())
526                    .with_context(|| format!("looking up {sub_dir:?}"))?,
527                Err(e) => bail!(e),
528            };
529        }
530
531        let flags = directory
532            .get_flags(zx::MonotonicInstant::INFINITE)
533            .context("transport error")?
534            .map_err(zx::Status::from_raw)
535            .context("get_flags")?;
536        let rights = flags.intersection(fio::MASK_KNOWN_PERMISSIONS);
537
538        let (client_end, server_end) = zx::Channel::create();
539        directory.clone(ServerEnd::new(server_end)).context("cloning directory")?;
540
541        // If a filesystem security label argument was provided then apply it to all files via
542        // mountpoint-labeling, with a "context=..." mount option.
543        let params = if let Some(mount_options) = mount_options {
544            MountParams::parse(mount_options.as_str().into())
545                .expect("failed to parse default_ns_mount_options")
546        } else {
547            MountParams::default()
548        };
549
550        let fs = RemoteFs::new_fs(
551            locked,
552            system_task.kernel(),
553            client_end,
554            FileSystemOptions { source: path.into(), params, ..Default::default() },
555            rights,
556        )
557        .context("making remote fs")?;
558
559        security::file_system_resolve_security(locked, system_task, &fs)
560            .context("resolving security")?;
561
562        // Fuchsia doesn't specify mount flags in the incoming namespace, so we need to make
563        // up some flags.
564        let flags = MountFlags::NOSUID | MountFlags::NODEV | MountFlags::RELATIME;
565        current_node.mount(WhatToMount::Fs(fs), flags.mountpoint_flags()).context("mounting fs")?;
566        self.mounts.push(current_node);
567
568        Ok(())
569    }
570
571    fn unmount(&mut self) -> Result<(), Errno> {
572        while let Some(node) = self.mounts.pop() {
573            node.unmount(UnmountFlags::DETACH)?;
574        }
575        Ok(())
576    }
577}
578
579impl Drop for MountRecord {
580    fn drop(&mut self) {
581        match self.unmount() {
582            Ok(()) => {}
583            Err(e) => log_error!("failed to unmount during component exit: {:?}", e),
584        }
585    }
586}