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