monitor/mon/compartment/
loader.rs

1use std::{collections::HashSet, ffi::CStr, ptr::null_mut};
2
3use dynlink::{
4    compartment::CompartmentId,
5    context::{Context, LoadIds, NewCompartmentFlags},
6    engines::LoadCtx,
7    library::{AllowedGates, LibraryId, UnloadedLibrary},
8    DynlinkError, SMALL_STRING_SIZE, SMALL_VEC_SIZE,
9};
10use happylock::ThreadKey;
11use monitor_api::SharedCompConfig;
12use smallstr::SmallString;
13use tinyvec::TinyVec;
14use twizzler_rt_abi::{
15    core::{CtorSet, RuntimeInfo},
16    error::{GenericError, TwzError},
17    object::{MapFlags, ObjID},
18};
19
20use super::{
21    CompConfigObject, CompartmentMgr, RunComp, StackObject, COMP_DESTRUCTED, COMP_EXITED,
22    COMP_IS_BINARY, COMP_READY, COMP_STARTED,
23};
24use crate::mon::{
25    get_monitor,
26    space::{MapHandle, Space},
27    thread::DEFAULT_STACK_SIZE,
28    Monitor,
29};
30
31/// Tracks info for loaded, but not yet running, compartments.
32#[derive(Debug)]
33pub struct RunCompLoader {
34    loaded_extras: dynlink::Vec<LoadInfo, SMALL_VEC_SIZE>,
35    root_comp: LoadInfo,
36}
37
38/// A single compartment, loaded but not yet running.
39#[derive(Debug, Clone)]
40struct LoadInfo {
41    // root (eg executable) library ID
42    #[allow(dead_code)]
43    root_id: LibraryId,
44    // runtime library ID (maybe injected)
45    #[allow(dead_code)]
46    rt_id: LibraryId,
47    // security context ID
48    sctx_id: ObjID,
49    name: SmallString<[u8; SMALL_STRING_SIZE]>,
50    comp_id: CompartmentId,
51    // all constructors for all libraries
52    ctor_info: dynlink::Vec<CtorSet, SMALL_VEC_SIZE>,
53    // entry point to call for the runtime to init this compartment
54    entry: Option<extern "C" fn(*const RuntimeInfo) -> !>,
55    is_binary: bool,
56}
57
58impl Default for LoadInfo {
59    fn default() -> Self {
60        Self {
61            root_id: LibraryId::default(),
62            rt_id: LibraryId::default(),
63            sctx_id: 0.into(),
64            name: "".into(),
65            comp_id: CompartmentId::default(),
66            ctor_info: dynlink::Vec::new(),
67            entry: None,
68            is_binary: false,
69        }
70    }
71}
72
73impl LoadInfo {
74    fn new(
75        dynlink: &mut Context,
76        root_id: LibraryId,
77        rt_id: LibraryId,
78        sctx_id: ObjID,
79        is_binary: bool,
80        extras: &[LibraryId],
81    ) -> Result<Self, DynlinkError> {
82        let lib = dynlink.get_library(rt_id)?;
83        let extra_ctors: Vec<_> = extras
84            .iter()
85            .map(|extra| dynlink.build_ctors_list(*extra, Some(lib.compartment())))
86            .try_collect()?;
87        let root_ctors = dynlink.build_ctors_list(root_id, Some(lib.compartment()))?;
88        let mut ctor_info: dynlink::Vec<_, SMALL_VEC_SIZE> =
89            extra_ctors.iter().flatten().copied().collect();
90        ctor_info.extend_from_slice(root_ctors.as_slice());
91        Ok(Self {
92            root_id,
93            rt_id,
94            comp_id: lib.compartment(),
95            sctx_id,
96            name: dynlink
97                .get_compartment(lib.compartment())?
98                .name
99                .as_str()
100                .into(),
101            ctor_info,
102            entry: Some(lib.get_entry_address()?),
103            is_binary,
104        })
105    }
106
107    fn build_runcomp(
108        &self,
109        handle: MapHandle,
110        stack_object: StackObject,
111        is_debugging: bool,
112    ) -> Result<RunComp, DynlinkError> {
113        let comp_config =
114            CompConfigObject::new(handle, SharedCompConfig::new(self.sctx_id, null_mut()));
115
116        let flags = if self.is_binary { COMP_IS_BINARY } else { 0 };
117        Ok(RunComp::new(
118            self.sctx_id,
119            self.sctx_id,
120            self.name.to_string(),
121            self.comp_id,
122            vec![],
123            comp_config,
124            flags,
125            stack_object,
126            self.entry.map(|x| x as usize).unwrap_or_default(),
127            &self.ctor_info,
128            is_debugging,
129        ))
130    }
131}
132
133impl Drop for RunCompLoader {
134    fn drop(&mut self) {
135        tracing::warn!("drop RunCompLoader: TODO");
136    }
137}
138
139const RUNTIME_NAME: &str = "libtwz_rt.so";
140
141impl RunCompLoader {
142    // the runtime library might be in the dependency tree from the shared object files.
143    // if not, we need to insert it.
144    fn maybe_inject_runtime(
145        dynlink: &mut Context,
146        root_id: LibraryId,
147        comp_id: CompartmentId,
148        load_ctx: &mut LoadCtx,
149    ) -> Result<LibraryId, DynlinkError> {
150        if let Some(id) = dynlink.lookup_library(comp_id, RUNTIME_NAME) {
151            return Ok(id);
152        }
153
154        let rt_unlib = UnloadedLibrary::new(RUNTIME_NAME);
155        let loads = dynlink.load_library_in_compartment(
156            comp_id,
157            rt_unlib,
158            AllowedGates::Private,
159            load_ctx,
160        )?;
161        dynlink.add_manual_dependency(root_id, loads[0].lib);
162        Ok(loads[0].lib)
163    }
164
165    /// Build a new RunCompLoader. This will load and relocate libraries in the dynamic linker, but
166    /// won't start compartment threads.
167    pub fn new(
168        dynlink: &mut Context,
169        comp_name: &str,
170        root_unlib: UnloadedLibrary,
171        extras: &[UnloadedLibrary],
172        new_comp_flags: NewCompartmentFlags,
173        mondebug: bool,
174    ) -> miette::Result<Self> {
175        struct UnloadOnDrop(TinyVec<[LoadIds; SMALL_VEC_SIZE]>);
176        impl Drop for UnloadOnDrop {
177            fn drop(&mut self) {
178                tracing::warn!("todo: drop");
179            }
180        }
181        let root_comp_id = dynlink.add_compartment(comp_name, new_comp_flags)?;
182        let allowed_gates = if new_comp_flags.contains(NewCompartmentFlags::EXPORT_GATES) {
183            AllowedGates::Public
184        } else {
185            AllowedGates::Private
186        };
187        let mut load_ctx = LoadCtx::default();
188
189        let extra_load_ids: Vec<_> = extras
190            .into_iter()
191            .map(|extra| {
192                if mondebug {
193                    tracing::info!("loading ld preload library: {}", extra.name);
194                } else {
195                    tracing::debug!("loading ld preload library: {}", extra.name);
196                }
197                dynlink.load_library_in_compartment(
198                    root_comp_id,
199                    extra.clone(),
200                    AllowedGates::Private,
201                    &mut load_ctx,
202                )
203            })
204            .try_collect()?;
205
206        let mut loads = UnloadOnDrop(dynlink.load_library_in_compartment(
207            root_comp_id,
208            root_unlib.clone(),
209            allowed_gates,
210            &mut load_ctx,
211        )?);
212
213        for extra in &extra_load_ids {
214            for extra in extra {
215                loads.0.push(extra.clone());
216            }
217        }
218
219        // The dynamic linker gives us a list of loaded libraries, and which compartments they ended
220        // up in. For each of those, we may need to inject the runtime library. Collect all
221        // the information about the extra compartments.
222        let mut cache = HashSet::new();
223        let extra_compartments = loads.0.iter().filter_map(|load| {
224            if load.comp != root_comp_id {
225                // This compartment was loaded in addition to the root comp as part of our
226                // initial load request. Check if we haven't seen it before.
227                if cache.contains(&load.comp) {
228                    return None;
229                }
230                cache.insert(load.comp);
231
232                // Inject the runtime library, careful to collect the error and keep going.
233                let rt_id =
234                    match Self::maybe_inject_runtime(dynlink, load.lib, load.comp, &mut load_ctx) {
235                        Ok(id) => id,
236                        Err(e) => return Some(Err(e)),
237                    };
238                Some(LoadInfo::new(
239                    dynlink,
240                    load.lib,
241                    rt_id,
242                    *load_ctx.set.get(&load.comp).unwrap(),
243                    false,
244                    &[],
245                ))
246            } else {
247                None
248            }
249        });
250
251        let extra_compartments = DynlinkError::collect(
252            dynlink::DynlinkErrorKind::CompartmentLoadFail {
253                compartment: comp_name.into(),
254            },
255            extra_compartments,
256        )?;
257
258        let root_id = loads.0[0].lib;
259        let rt_id = Self::maybe_inject_runtime(dynlink, root_id, root_comp_id, &mut load_ctx)?;
260        let extra_lids = extra_load_ids
261            .iter()
262            .flatten()
263            .map(|x| x.lib)
264            .collect::<Vec<_>>();
265        for extra in &extra_lids {
266            dynlink.relocate_all(*extra)?;
267        }
268        dynlink.relocate_all(root_id)?;
269
270        let is_binary = dynlink.get_library(root_id)?.is_binary();
271        let root_comp = LoadInfo::new(
272            dynlink,
273            root_id,
274            rt_id,
275            *load_ctx.set.get(&root_comp_id).unwrap(),
276            is_binary,
277            extra_lids.as_slice(),
278        )?;
279
280        if mondebug {
281            let print_comp = |cmp: &LoadInfo| -> miette::Result<()> {
282                let dcmp = dynlink.get_compartment(cmp.comp_id)?;
283                tracing::info!("Loaded libraries for {}:", &dcmp.name);
284                for lid in dcmp.library_ids() {
285                    let lib = dynlink.get_library(lid)?;
286                    let mut flags = ["-", "-", "-"];
287                    if lib.is_binary() {
288                        flags[0] = "B";
289                    } else {
290                        flags[0] = "l";
291                    }
292                    if lib.id() == cmp.rt_id {
293                        flags[1] = "r";
294                    } else if lib.id() == cmp.root_id {
295                        flags[1] = "R";
296                    }
297                    if lib.allows_gates() {
298                        flags[2] = "g";
299                    }
300                    let flags = flags.join("");
301                    tracing::info!("{:16x} {} {}", lib.base_addr(), flags, &lib.name);
302                    if let Some(isg) = lib.iter_secgates() {
303                        for gate in isg {
304                            tracing::info!(
305                                "    GATE {:16x} {}",
306                                gate.imp,
307                                gate.name().to_string_lossy()
308                            )
309                        }
310                    }
311                }
312                Ok(())
313            };
314            tracing::info!("Load info for {}", comp_name);
315            let _ = print_comp(&root_comp);
316            for cmp in &extra_compartments {
317                let _ = print_comp(cmp);
318            }
319        }
320
321        // We don't want to drop anymore, since now drop-cleanup will be handled by RunCompLoader.
322        std::mem::forget(loads);
323        Ok(RunCompLoader {
324            loaded_extras: extra_compartments,
325            root_comp,
326        })
327    }
328
329    pub fn build_rcs(
330        self,
331        cmp: &mut CompartmentMgr,
332        dynlink: &mut Context,
333        mondebug: bool,
334        is_debugging: bool,
335    ) -> miette::Result<ObjID> {
336        let make_new_handle = |ty, id| {
337            if mondebug {
338                tracing::info!(
339                    "creating runtime {} object {} for compartment {}",
340                    ty,
341                    id,
342                    self.root_comp.name
343                );
344            }
345            Space::safe_create_and_map_runtime_object(
346                &get_monitor().space,
347                id,
348                MapFlags::READ | MapFlags::WRITE,
349            )
350        };
351        let stack = StackObject::new(
352            make_new_handle("stack", self.root_comp.sctx_id)?,
353            DEFAULT_STACK_SIZE,
354        )?;
355
356        let root_rc = self.root_comp.build_runcomp(
357            make_new_handle("comp-config", self.root_comp.sctx_id)?,
358            stack,
359            is_debugging,
360        )?;
361
362        let mut ids = vec![root_rc.instance];
363        // Make all the handles first, for easier cleanup.
364        let handles = self
365            .loaded_extras
366            .iter()
367            .map(|extra| {
368                let stack =
369                    StackObject::new(make_new_handle("stack", extra.sctx_id)?, DEFAULT_STACK_SIZE)?;
370                Ok::<_, miette::Report>((make_new_handle("comp-config", extra.sctx_id)?, stack))
371            })
372            .try_collect::<Vec<_>>()?;
373        // Construct the RunComps for all the extra compartments.
374        let mut extras = self
375            .loaded_extras
376            .iter()
377            .zip(handles)
378            .map(|extra| extra.0.build_runcomp(extra.1 .0, extra.1 .1, false))
379            .try_collect::<Vec<_>>()?;
380
381        for rc in extras.drain(..) {
382            ids.push(rc.instance);
383            cmp.insert(rc);
384        }
385        cmp.insert(root_rc);
386        std::mem::forget(self);
387
388        // Set all the dependency information.
389        for id in &ids {
390            let Ok(comp) = cmp.get(*id) else { continue };
391            let mut deps = dynlink
392                .compartment_dependencies(comp.compartment_id)?
393                .iter()
394                .filter_map(|item| cmp.get_dynlinkid(*item).map(|rc| rc.instance).ok())
395                .collect();
396            cmp.get_mut(*id).unwrap().deps.append(&mut deps);
397
398            let Ok(comp) = cmp.get(*id) else { continue };
399            tracing::debug!("set comp {} deps to {:?}", comp.name, comp.deps);
400        }
401        Self::rec_inc_all_use_counts(cmp, ids[0], &HashSet::from_iter(ids.iter().cloned()));
402
403        Ok(ids[0])
404    }
405
406    fn rec_inc_all_use_counts(
407        cmgr: &mut CompartmentMgr,
408        start: ObjID,
409        created: &HashSet<ObjID>,
410    ) -> Option<()> {
411        debug_assert!(created.contains(&start));
412        let rc = cmgr.get(start).ok()?;
413        for dep in rc.deps.clone() {
414            if created.contains(&dep) {
415                Self::rec_inc_all_use_counts(cmgr, dep, created);
416            }
417            if let Ok(rc) = cmgr.get_mut(dep) {
418                rc.inc_use_count();
419            }
420        }
421
422        Some(())
423    }
424}
425
426impl Monitor {
427    pub(crate) fn start_compartment(
428        &self,
429        instance: ObjID,
430        args: &[&CStr],
431        env: &[&CStr],
432        mondebug: bool,
433        suspend_on_start: bool,
434    ) -> Result<(), TwzError> {
435        if mondebug {
436            tracing::info!("start compartment {}: {:?} {:?}", instance, args, env);
437        }
438        let deps = {
439            let cmp = self.comp_mgr.read(ThreadKey::get().unwrap());
440            let rc = cmp.get(instance)?;
441            tracing::debug!(
442                "starting compartment {} ({}) (binary = {})",
443                rc.name,
444                rc.instance,
445                rc.has_flag(COMP_IS_BINARY)
446            );
447            rc.deps.clone()
448        };
449        for dep in deps {
450            self.start_compartment(dep, &[], env, false, false)?;
451        }
452        // Check the state of this compartment.
453        let state = self.load_compartment_flags(instance);
454        if state & COMP_EXITED != 0 || state & COMP_DESTRUCTED != 0 {
455            tracing::error!(
456                "tried to start compartment ({:?}, {}) that has already exited (state: {:x})",
457                self.comp_name(instance),
458                instance,
459                state
460            );
461            return Err(GenericError::Internal.into());
462        }
463
464        loop {
465            // Check the state of this compartment.
466            let state = self.load_compartment_flags(instance);
467            if state & COMP_READY != 0 {
468                return Ok(());
469            }
470            if suspend_on_start {
471                // We can't wait for ready, since that need the thread to run.
472                if state & COMP_STARTED != 0 {
473                    return Ok(());
474                }
475            }
476            let info = {
477                let (ref mut tmgr, ref mut cmp, ref mut dynlink, _, _) =
478                    *self.locks.lock(ThreadKey::get().unwrap());
479                let rc = cmp.get_mut(instance)?;
480
481                rc.start_main_thread(
482                    state,
483                    &mut *tmgr,
484                    &mut *dynlink,
485                    args,
486                    env,
487                    suspend_on_start,
488                )
489            };
490            if info.is_none() {
491                return Err(GenericError::Internal.into());
492            }
493            self.wait_for_compartment_state_change(instance, state);
494        }
495    }
496}