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, parse_rlimits, run_component_features};
6use anyhow::{Context, Error, anyhow, bail};
7use fidl::AsyncChannel;
8use fidl::endpoints::{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::{RngExt as _, 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::errors::{EEXIST, ENOTDIR, Errno};
38use starnix_uapi::file_mode::mode;
39use starnix_uapi::mount_flags::{MountFlags, MountpointFlags};
40use starnix_uapi::open_flags::OpenFlags;
41use starnix_uapi::signals::{SIGINT, SIGKILL};
42use starnix_uapi::unmount_flags::UnmountFlags;
43use starnix_uapi::{errno, rlimit};
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)]
81    rlimits: Vec<String>,
82
83    #[serde(default, deserialize_with = "parse_capabilities")]
84    capabilities: Option<Capabilities>,
85
86    #[serde(default)]
87    seclabel: Option<CString>,
88
89    #[serde(default, rename(deserialize = "test_target_kernel"))]
90    _test_target_kernel: Option<String>,
91}
92
93impl ComponentProgram {
94    fn resolve_templates(&mut self, component_path: &str, pkg_path: &str) {
95        let resolve_template = |values: &mut Vec<String>| {
96            for val in values {
97                *val = val
98                    .replace("{pkg_path}", &pkg_path)
99                    .replace("{component_path}", &component_path);
100            }
101        };
102
103        resolve_template(&mut self.args);
104        resolve_template(&mut self.environ);
105    }
106}
107
108fn parse_capabilities<'de, D>(deserializer: D) -> Result<Option<Capabilities>, D::Error>
109where
110    D: serde::Deserializer<'de>,
111{
112    let mut capabilities = Capabilities::empty();
113    for cap in Vec::<String>::deserialize(deserializer)? {
114        capabilities |= cap.parse().map_err(D::Error::custom)?;
115    }
116    Ok(Some(capabilities))
117}
118
119/// Starts a component inside the given container.
120///
121/// The component's `binary` can either:
122///   - an absolute path, in which case the path is treated as a path into the root filesystem that
123///     is mounted by the container's configuration
124///   - relative path, in which case the binary is read from the component's package (which is
125///     mounted at /container/component/{random}/pkg.)
126///
127/// The directories in the component's namespace are mounted at /container/component/{random}.
128pub async fn start_component(
129    mut start_info: ComponentStartInfo,
130    controller: ServerEnd<ComponentControllerMarker>,
131    system_task: &CurrentTask,
132) -> Result<(), Error> {
133    let url = start_info.resolved_url.clone().unwrap_or_else(|| "<unknown>".to_string());
134
135    let (task_complete_sender, task_complete) = oneshot::channel::<TaskResult>();
136
137    let weak_task = system_task.override_creds(
138        security::creds_start_internal_operation(system_task),
139        || {
140            // TODO(https://fxbug.dev/42076551): We leak the directory created by this function.
141            let component_path = generate_component_path(system_task)?;
142            let pkg_path = format!("{component_path}/pkg");
143
144            let mount_record =
145                Arc::new(LockDepMutex::<_, ComponentMountRecordLock>::new(MountRecord::default()));
146
147            let ns = start_info.ns.take().ok_or_else(|| anyhow!("Missing namespace"))?;
148
149            let program = start_info.program.as_ref().context("reading program block")?;
150            let mut program: ComponentProgram =
151                runner::serde::deserialize_program(program).context("parsing program block")?;
152            let rlimits = parse_rlimits(&program.rlimits)?;
153            program.resolve_templates(&component_path, &pkg_path);
154            log_debug!("start_component: {}\n{:#?}", url, program);
155
156            let ns_mount_options = system_task.kernel().features.default_ns_mount_options.as_ref();
157            let mut maybe_pkg = None;
158            let mut maybe_svc = None;
159            for entry in ns {
160                if let (Some(dir_path), Some(dir_handle)) = (entry.path, entry.directory) {
161                    let dir_path_str = dir_path.as_str();
162                    let mount_options = ns_mount_options
163                        .and_then(|mount_options| mount_options.get(dir_path_str).cloned());
164
165                    match dir_path_str {
166                        "/svc" => {
167                            maybe_svc = Some(fio::DirectoryProxy::new(AsyncChannel::from_channel(
168                                dir_handle.into_channel(),
169                            )));
170                        }
171                        "/custom_artifacts" => {
172                            // Mount custom_artifacts directory at root of container
173                            // We may want to transition to have this directory unique per component
174                            let dir_proxy =
175                                fio::DirectorySynchronousProxy::new(dir_handle.into_channel());
176                            mount_record
177                                .lock()
178                                .mount_remote(
179                                    system_task,
180                                    &dir_proxy,
181                                    &dir_path,
182                                    mount_options.as_ref(),
183                                )
184                                .with_context(|| {
185                                    format!("failed to mount_remote on path {}", dir_path)
186                                })?;
187                        }
188                        _ => {
189                            let dir_proxy =
190                                fio::DirectorySynchronousProxy::new(dir_handle.into_channel());
191                            mount_record
192                                .lock()
193                                .mount_remote(
194                                    system_task,
195                                    &dir_proxy,
196                                    &format!("{component_path}/{dir_path}"),
197                                    mount_options.as_ref(),
198                                )
199                                .with_context(|| {
200                                    format!(
201                                        "failed to mount_remote on path {component_path}/{dir_path}"
202                                    )
203                                })?;
204                            if dir_path == "/pkg" {
205                                maybe_pkg = Some(dir_proxy);
206                            }
207                        }
208                    }
209                }
210            }
211
212            let pkg = maybe_pkg.ok_or_else(|| anyhow!("Missing /pkg entry in namespace"))?;
213
214            let uid = program
215                .uid
216                .map(|uid| uid.0)
217                .unwrap_or_else(|| system_task.kernel().features.default_uid);
218
219            let mut credentials = Credentials::with_ids(uid, uid);
220            if let Some(capabilities) = program.capabilities {
221                credentials.cap_permitted = capabilities;
222                credentials.cap_effective = capabilities;
223                credentials.cap_inheritable = capabilities;
224                credentials.cap_ambient = capabilities;
225            }
226
227            run_component_features(system_task.kernel(), &program.features, maybe_svc)
228                .unwrap_or_else(|e| {
229                    log_error!("failed to set component features for {} - {:?}", url, e);
230                });
231
232            let current_task = create_init_child_process(
233                system_task.kernel(),
234                TaskCommand::new(program.binary.as_bytes()),
235                credentials,
236                program.seclabel.as_ref(),
237            )?;
238            // `create_init_child_process` clones the `init` task's resource limits (which include
239            // the container's `program.rlimits`). Apply the component's `program.rlimits` here to
240            // override those container-level defaults.
241            {
242                let mut limits = current_task.thread_group().limits.lock();
243                for (resource, limit) in rlimits {
244                    limits.set(resource, rlimit { rlim_cur: limit, rlim_max: limit });
245                }
246            }
247
248            execute_task_with_prerun_result(
249                current_task,
250                {
251                    let mount_record = mount_record.clone();
252                    move |current_task| {
253                        let cwd_path = FsString::from(program.cwd.unwrap_or(pkg_path));
254                        let cwd = current_task.lookup_path(
255                            &mut LookupContext::default(),
256                            current_task.fs().root(),
257                            cwd_path.as_ref(),
258                        )?;
259                        current_task.fs().chdir(current_task, cwd)?;
260
261                        for mount in &program.component_mounts {
262                            let Some(action) = MountAction::from_spec(current_task, &pkg, mount)
263                                .map_err(|e| {
264                                    log_error!("Error while mounting the filesystems: {e:?}");
265                                    errno!(EINVAL)
266                                })?
267                            else {
268                                continue;
269                            };
270                            let mount_point =
271                                current_task.lookup_path_from_root(action.path.as_ref())?;
272                            mount_record.lock().mount(
273                                mount_point,
274                                WhatToMount::Fs(action.fs),
275                                action.flags,
276                            )?;
277                        }
278
279                        let files = current_task.files();
280                        parse_numbered_handles(current_task, start_info.numbered_handles, &files)
281                            .map_err(|e| {
282                            log_error!("Error while parsing the numbered handles: {e:?}");
283                            errno!(EINVAL)
284                        })?;
285
286                        let mut argv = vec![program.binary.clone()];
287                        for arg in program.args {
288                            argv.push(CString::new(arg).map_err(|_| errno!(EINVAL))?);
289                        }
290
291                        let mut environ = vec![];
292                        for env in program.environ {
293                            environ.push(CString::new(env).map_err(|_| errno!(EINVAL))?);
294                        }
295
296                        let executable = current_task.open_file_for_exec(
297                            FdNumber::AT_FDCWD,
298                            program.binary.as_bytes().into(),
299                            OpenFlags::empty(),
300                        )?;
301                        current_task.exec(executable, program.binary, argv, environ)?;
302
303                        Ok(Arc::downgrade(&current_task.task))
304                    }
305                },
306                move |result| {
307                    // Unmount all the directories for this component.
308                    std::mem::drop(mount_record);
309
310                    // If the component controller server has gone away, there is nobody for us to
311                    // report the result to.
312                    let _ = task_complete_sender.send(result);
313                },
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.as_ref(),
359                            signals::SignalInfo::kernel(SIGINT),
360                        );
361                        log_info!("Sent SIGINT to program {}", task.command());
362                    }
363                }
364                Ok(ComponentControllerRequest::Kill { .. }) => {
365                    if let Some(task) = task.upgrade() {
366                        signals::send_standard_signal(&task, signals::SignalInfo::forced(SIGKILL));
367                        log_info!("Sent SIGKILL to program {}", task.command());
368                        controller_handle.shutdown_with_epitaph(zx::Status::err_from_raw(
369                            fcomponent::Error::InstanceDied.into_primitive() as i32,
370                        ));
371                    }
372                    return;
373                }
374                Ok(ComponentControllerRequest::_UnknownMethod { ordinal, .. }) => {
375                    log_warn!("Unknown ComponentController request: {ordinal}");
376                }
377                Err(_) => {
378                    return;
379                }
380            },
381            Event::Completion(result) => match result {
382                Ok(Ok(ExitStatus::Exit(0))) => controller_handle.shutdown_with_epitaph(Ok(())),
383                Ok(Ok(ExitStatus::Exit(n))) => {
384                    let epitaph =
385                        zx::Status::try_from_raw(COMPONENT_EXIT_CODE_BASE + n as i32).unwrap();
386                    controller_handle.shutdown_with_epitaph(Err(epitaph));
387                }
388                _ => {
389                    let epitaph = zx::Status::try_from_raw(
390                        fcomponent::Error::InstanceDied.into_primitive() as i32,
391                    )
392                    .unwrap();
393                    controller_handle.shutdown_with_epitaph(Err(epitaph));
394                }
395            },
396        }
397    }
398}
399
400/// Returns /container/component/{random} that doesn't already exist
401fn generate_component_path(system_task: &CurrentTask) -> Result<String, Error> {
402    // Checking container directory already exists.
403    // If this lookup fails, the container might not have the "container" feature enabled.
404    let mount_point = system_task.lookup_path_from_root("/container/component/".into())?;
405
406    // Find /container/component/{random} that doesn't already exist
407    let component_path = loop {
408        let random_string: String =
409            rng().sample_iter(&Alphanumeric).take(10).map(char::from).collect();
410
411        // This returns EEXIST if /container/component/{random} already exists.
412        // If so, try again with another {random} string.
413        match mount_point.create_node(
414            system_task,
415            random_string.as_str().into(),
416            mode!(IFDIR, 0o755),
417            DeviceId::NONE,
418        ) {
419            Ok(_) => break format!("/container/component/{random_string}"),
420            Err(errno) if errno == EEXIST => {}
421            Err(e) => bail!(e),
422        };
423    };
424
425    Ok(component_path)
426}
427
428/// Adds the given startup handles to a CurrentTask.
429///
430/// The `numbered_handles` of type `HandleType::FileDescriptor` are used to
431/// create files, and the handles are required to be of type `zx::Socket`.
432///
433/// If there is a `numbered_handles` of type `HandleType::User0`, that is
434/// interpreted as the server end of the ShellController protocol.
435pub fn parse_numbered_handles(
436    current_task: &CurrentTask,
437    numbered_handles: Option<Vec<fprocess::HandleInfo>>,
438    files: &FdTable,
439) -> Result<(), Error> {
440    if let Some(numbered_handles) = numbered_handles {
441        for numbered_handle in numbered_handles {
442            let info = HandleInfo::try_from(numbered_handle.id)?;
443            if info.handle_type() == HandleType::FileDescriptor {
444                let file = create_file_from_handle(current_task, numbered_handle.handle)?;
445                files.insert(current_task, FdNumber::from_raw(info.arg().into()), file)?;
446            }
447        }
448    }
449
450    let stdio = SyslogFile::new_file(current_task);
451    // If no numbered handle is provided for each stdio handle, default to syslog.
452    for i in [0, 1, 2] {
453        if files.get(FdNumber::from_raw(i)).is_err() {
454            files.insert(current_task, FdNumber::from_raw(i), stdio.clone())?;
455        }
456    }
457
458    Ok(())
459}
460
461/// A record of the mounts created when starting a component.
462///
463/// When the record is dropped, the mounts are unmounted.
464#[derive(Default)]
465struct MountRecord {
466    /// The namespace nodes at which we have crated mounts for this component.
467    mounts: Vec<NamespaceNode>,
468}
469
470impl MountRecord {
471    fn mount(
472        &mut self,
473        mount_point: NamespaceNode,
474        what: WhatToMount,
475        flags: MountpointFlags,
476    ) -> Result<(), Errno> {
477        mount_point.mount(what, flags.into())?;
478        self.mounts.push(mount_point);
479        Ok(())
480    }
481
482    fn mount_remote(
483        &mut self,
484        system_task: &CurrentTask,
485        directory: &fio::DirectorySynchronousProxy,
486        path: &str,
487        mount_options: Option<&String>,
488    ) -> Result<(), Error> {
489        // The incoming dir_path might not be top level, e.g. it could be /foo/bar.
490        // Iterate through each component directory starting from the parent and
491        // create it if it doesn't exist.
492        let mut current_node =
493            system_task.lookup_path_from_root(".".into()).context("looking up '.'")?;
494        let mut context = LookupContext::default();
495
496        // Extract each component using Path::new(path).components(). For example,
497        // Path::new("/foo/bar").components() will return [RootDir, Normal("foo"), Normal("bar")].
498        // We're not interested in the RootDir, so we drop the prefix "/" if it exists.
499        let path = if let Some(path) = path.strip_prefix('/') { path } else { path };
500
501        for sub_dir in Path::new(path).components() {
502            let sub_dir_bytes = sub_dir.as_os_str().as_bytes();
503            current_node = match current_node.create_node(
504                system_task,
505                sub_dir_bytes.into(),
506                mode!(IFDIR, 0o755),
507                DeviceId::NONE,
508            ) {
509                Ok(node) => node,
510                Err(errno) if errno == EEXIST || errno == ENOTDIR => current_node
511                    .lookup_child(system_task, &mut context, sub_dir_bytes.into())
512                    .with_context(|| format!("looking up {sub_dir:?}"))?,
513                Err(e) => bail!(e),
514            };
515        }
516
517        let flags = directory
518            .get_flags(zx::MonotonicInstant::INFINITE)
519            .context("transport error")?
520            .map_err(zx::Status::err_from_raw)
521            .context("get_flags")?;
522        let rights = flags.intersection(fio::MASK_KNOWN_PERMISSIONS);
523
524        let (client_end, server_end) = zx::Channel::create();
525        directory.clone(ServerEnd::new(server_end)).context("cloning directory")?;
526
527        // If a filesystem security label argument was provided then apply it to all files via
528        // mountpoint-labeling, with a "context=..." mount option.
529        let params = if let Some(mount_options) = mount_options {
530            MountParams::parse(mount_options.as_str().into())
531                .expect("failed to parse default_ns_mount_options")
532        } else {
533            MountParams::default()
534        };
535
536        let fs = RemoteFs::new_fs(
537            system_task.kernel(),
538            client_end,
539            FileSystemOptions { source: path.into(), params, ..Default::default() },
540            rights,
541        )
542        .context("making remote fs")?;
543
544        security::file_system_resolve_security(system_task, &fs).context("resolving security")?;
545
546        // Fuchsia doesn't specify mount flags in the incoming namespace, so we need to make
547        // up some flags.
548        let flags = MountFlags::NOSUID | MountFlags::NODEV | MountFlags::RELATIME;
549        current_node.mount(WhatToMount::Fs(fs), flags.mountpoint_flags()).context("mounting fs")?;
550        self.mounts.push(current_node);
551
552        Ok(())
553    }
554
555    fn unmount(&mut self) -> Result<(), Errno> {
556        while let Some(node) = self.mounts.pop() {
557            node.unmount(UnmountFlags::DETACH)?;
558        }
559        Ok(())
560    }
561}
562
563impl Drop for MountRecord {
564    fn drop(&mut self) {
565        match self.unmount() {
566            Ok(()) => {}
567            Err(e) => log_error!("failed to unmount during component exit: {:?}", e),
568        }
569    }
570}
571
572#[cfg(test)]
573mod test {
574    use super::ComponentProgram;
575    use fidl_fuchsia_data as fdata;
576    use std::ffi::CString;
577
578    #[fuchsia::test]
579    fn test_deserialize_component_program_rlimits() {
580        let program_with_rlimits = fdata::Dictionary {
581            entries: Some(vec![
582                fdata::DictionaryEntry {
583                    key: "binary".to_string(),
584                    value: Some(Box::new(fdata::DictionaryValue::Str("/bin/sh".to_string()))),
585                },
586                fdata::DictionaryEntry {
587                    key: "rlimits".to_string(),
588                    value: Some(Box::new(fdata::DictionaryValue::StrVec(vec![
589                        "RLIMIT_NOFILE=131072".to_string(),
590                        "RLIMIT_RTPRIO=10".to_string(),
591                    ]))),
592                },
593            ]),
594            ..Default::default()
595        };
596        let parsed: ComponentProgram =
597            runner::serde::deserialize_program(&program_with_rlimits).expect("deserialize_program");
598        assert_eq!(parsed.binary, CString::new("/bin/sh").unwrap());
599        assert_eq!(
600            parsed.rlimits,
601            vec!["RLIMIT_NOFILE=131072".to_string(), "RLIMIT_RTPRIO=10".to_string()]
602        );
603
604        let program_without_rlimits = fdata::Dictionary {
605            entries: Some(vec![fdata::DictionaryEntry {
606                key: "binary".to_string(),
607                value: Some(Box::new(fdata::DictionaryValue::Str("/bin/sh".to_string()))),
608            }]),
609            ..Default::default()
610        };
611        let parsed: ComponentProgram = runner::serde::deserialize_program(&program_without_rlimits)
612            .expect("deserialize_program");
613        assert!(parsed.rlimits.is_empty());
614    }
615}