Skip to main content

alef_component_runtime/
loader.rs

1use crate::{
2    ArtifactCache, COMPONENT_ABI_VERSION, COMPONENT_MANIFEST_SCHEMA, ComponentError, ComponentLockEntry,
3    ComponentManifest,
4};
5use alef_component_abi::{
6    ABI_MAJOR_V1, ABI_MINOR_V1, AlefComponentEntryV1, AlefComponentV1, AlefContract, AlefHostApiV1, AlefOwnedBuffer,
7};
8use libloading::Library;
9use std::ffi::c_void;
10use std::marker::PhantomData;
11use std::ptr::NonNull;
12use std::sync::{Arc, Mutex, OnceLock};
13
14const MAX_DESCRIPTOR_STRING: usize = 16 * 1024;
15
16/// What a loaded component's descriptor must match.
17///
18/// `contract_name` identifies which of a component's `provides` entries this
19/// is for; `contract_hash` is that specific contract's hash, not the
20/// component-wide legacy `ComponentIdentity::contract_hash`. A component that
21/// provides several contracts is loaded once per contract, each through its
22/// own entry point (see [`crate::ComponentManager::ensure_contract`]), so each
23/// load only ever validates one contract's requirements.
24#[derive(Clone, Debug)]
25pub struct ComponentRequirements {
26    pub component_id: String,
27    pub contract_name: String,
28    pub contract_hash: [u8; 32],
29    pub feature_set_hash: Option<[u8; 32]>,
30}
31
32#[derive(Clone, Debug)]
33pub struct Runtime {
34    cache: ArtifactCache,
35}
36
37impl Runtime {
38    pub fn new(cache: ArtifactCache) -> Self {
39        Self { cache }
40    }
41
42    pub fn install_and_load(
43        &self,
44        entry: &ComponentLockEntry,
45        entry_symbol: &[u8],
46        requirements: &ComponentRequirements,
47        host: AlefHostApiV1,
48    ) -> Result<LoadedComponent, ComponentError> {
49        let cached = self.cache.install(entry)?;
50        validate_manifest(&cached.manifest, requirements)?;
51        LoadedComponent::load(cached.library, entry_symbol, requirements, host)
52    }
53}
54
55pub struct LoadedComponent {
56    library: Arc<Library>,
57    descriptor: NonNull<AlefComponentV1>,
58    host: Box<AlefHostApiV1>,
59    component_id: String,
60    component_version: String,
61}
62
63/// A live instance created by a component, paired with its typed C function table.
64///
65/// Dropping the instance invokes the component's own destructor while the
66/// originating dynamic library remains pinned.
67pub struct ComponentInstance<T: AlefContract> {
68    component: Arc<LoadedComponent>,
69    handle: NonNull<c_void>,
70    _contract: PhantomData<T>,
71}
72
73// SAFETY: the descriptor is static component metadata and the library is pinned
74// for the process lifetime. Host callback context thread-safety is part of the
75// `AlefHostApiV1` contract supplied by the caller. ~keep
76unsafe impl Send for LoadedComponent {}
77unsafe impl Sync for LoadedComponent {}
78
79// SAFETY: the C ABI requires every contract method to take `&self` (see
80// `component::map_method`) and calls it through a plain function pointer, so the
81// component's own implementation is the only thing that can make concurrent access
82// unsound; the ABI async wrapper already calls back into an instance from a spawned
83// worker thread, so components are contractually required to tolerate this. The
84// instance handle itself is owned exclusively by this `ComponentInstance` for its
85// lifetime, with no interior aliasing on the host side. ~keep
86unsafe impl<T: AlefContract> Send for ComponentInstance<T> {}
87unsafe impl<T: AlefContract> Sync for ComponentInstance<T> {}
88
89impl LoadedComponent {
90    /// Load one contract's descriptor from a component's shared library.
91    ///
92    /// A component that provides several contracts exports one entry point
93    /// per contract (`alef_component_entry_v1_<contract>`, see
94    /// `alef::codegen::component::entry_point_symbol`); `entry_symbol` is that
95    /// NUL-terminated symbol name, so this loads exactly one contract's
96    /// descriptor per call even when the underlying `.so`/`.dylib`/`.dll` is
97    /// shared across several `LoadedComponent`s.
98    pub fn load(
99        path: impl Into<std::path::PathBuf>,
100        entry_symbol: &[u8],
101        requirements: &ComponentRequirements,
102        mut host: AlefHostApiV1,
103    ) -> Result<Self, ComponentError> {
104        let path = path.into();
105        host.struct_size = core::mem::size_of::<AlefHostApiV1>();
106        host.abi_major = ABI_MAJOR_V1;
107        host.abi_minor = ABI_MINOR_V1;
108        let host = Box::new(host);
109
110        let library = Arc::new(
111            unsafe { Library::new(&path) }.map_err(|error| ComponentError::LibraryLoad {
112                path: path.clone(),
113                message: error.to_string(),
114            })?,
115        );
116        let descriptor = unsafe {
117            let entry: libloading::Symbol<'_, AlefComponentEntryV1> = library
118                .get(entry_symbol)
119                .map_err(|error| ComponentError::MissingEntrypoint(error.to_string()))?;
120            let mut raw = core::ptr::null();
121            let status = entry(ABI_MAJOR_V1, host.as_ref(), &mut raw);
122            if !status.is_ok() {
123                return Err(ComponentError::EntrypointFailed(status.0));
124            }
125            NonNull::new(raw.cast_mut()).ok_or(ComponentError::InvalidDescriptor("null descriptor"))?
126        };
127
128        // Reject a too-small descriptor before a `&AlefComponentV1` reference is ever formed
129        // over it: forming that reference asserts the pointee is valid for
130        // `size_of::<AlefComponentV1>()` bytes, which is undefined behavior when the
131        // component actually returned a shorter, older-ABI struct.
132        unsafe { reject_undersized_descriptor(descriptor)? };
133        let validated = unsafe { validate_descriptor(descriptor.as_ref(), requirements)? };
134        pin_for_process(Arc::clone(&library));
135        Ok(Self {
136            library,
137            descriptor,
138            host,
139            component_id: validated.0,
140            component_version: validated.1,
141        })
142    }
143
144    #[must_use]
145    pub fn component_id(&self) -> &str {
146        &self.component_id
147    }
148
149    #[must_use]
150    pub fn component_version(&self) -> &str {
151        &self.component_version
152    }
153
154    #[must_use]
155    pub fn descriptor(&self) -> &AlefComponentV1 {
156        unsafe { self.descriptor.as_ref() }
157    }
158
159    #[must_use]
160    pub fn contract(&self) -> *const c_void {
161        self.descriptor().contract
162    }
163
164    #[must_use]
165    pub fn contract_size(&self) -> usize {
166        self.descriptor().contract_size
167    }
168
169    pub fn contract_table<T: AlefContract>(&self) -> Result<&T, ComponentError> {
170        contract_table_from_descriptor(self.descriptor())
171    }
172
173    /// Create an instance and bind it to a generated contract-table type.
174    pub fn instantiate<T: AlefContract>(self: &Arc<Self>) -> Result<ComponentInstance<T>, ComponentError> {
175        self.contract_table::<T>()?;
176        let create = self.descriptor().create.ok_or(ComponentError::MissingInstanceFactory)?;
177        self.descriptor()
178            .destroy
179            .ok_or(ComponentError::MissingInstanceDestructor)?;
180        let mut raw = core::ptr::null_mut();
181        let mut error = AlefOwnedBuffer::EMPTY;
182        let status = unsafe { create(self.host_api(), &mut raw, &mut error) };
183        if !status.is_ok() {
184            return Err(ComponentError::InstanceCreation {
185                status: status.0,
186                message: unsafe { take_owned_buffer(error) },
187            });
188        }
189        let handle = NonNull::new(raw).ok_or(ComponentError::NullInstance)?;
190        Ok(ComponentInstance {
191            component: Arc::clone(self),
192            handle,
193            _contract: PhantomData,
194        })
195    }
196
197    #[must_use]
198    pub fn host_api(&self) -> &AlefHostApiV1 {
199        &self.host
200    }
201
202    #[must_use]
203    pub fn strong_library_references(&self) -> usize {
204        Arc::strong_count(&self.library)
205    }
206}
207
208impl<T: AlefContract> ComponentInstance<T> {
209    #[must_use]
210    pub fn table(&self) -> &T {
211        // The table was validated before this instance was constructed.
212        self.component
213            .contract_table::<T>()
214            .expect("validated component contract table changed")
215    }
216
217    #[must_use]
218    pub fn handle(&self) -> *mut c_void {
219        self.handle.as_ptr()
220    }
221
222    #[must_use]
223    pub fn component(&self) -> &Arc<LoadedComponent> {
224        &self.component
225    }
226}
227
228impl<T: AlefContract> Drop for ComponentInstance<T> {
229    fn drop(&mut self) {
230        if let Some(destroy) = self.component.descriptor().destroy {
231            unsafe { destroy(self.handle.as_ptr()) };
232        }
233    }
234}
235
236/// Copy an owned buffer's bytes into a `Vec<u8>` and release the original
237/// allocation through its own `free` callback.
238///
239/// Generated contract proxies use this to convert a component's
240/// `AlefOwnedBuffer` results into owned Rust values without re-implementing
241/// buffer ownership handling in every generated snippet.
242///
243/// # Safety
244///
245/// `buffer` must be a valid `AlefOwnedBuffer`: `ptr` must be non-null and
246/// valid for `len` bytes whenever `len > 0`, and `free` (if present) must be
247/// safe to call with this buffer's own `context`, `ptr`, `len`, and
248/// `capacity`.
249#[must_use]
250pub unsafe fn take_owned_bytes(buffer: AlefOwnedBuffer) -> Vec<u8> {
251    let bytes = if buffer.ptr.is_null() || buffer.len == 0 {
252        Vec::new()
253    } else {
254        // SAFETY: upheld by this function's caller contract.
255        unsafe { core::slice::from_raw_parts(buffer.ptr, buffer.len) }.to_vec()
256    };
257    if let Some(free) = buffer.free {
258        // SAFETY: upheld by this function's caller contract.
259        unsafe { free(buffer.context, buffer.ptr, buffer.len, buffer.capacity) };
260    }
261    bytes
262}
263
264/// Like [`take_owned_bytes`], but lossily decodes the bytes as UTF-8.
265///
266/// Generated proxies use this to turn a component's error buffer into a
267/// `String` for constructing the contract's error type; a component that
268/// returns non-UTF-8 error text gets a replacement-character message instead
269/// of a panic or a dropped allocation.
270///
271/// # Safety
272///
273/// Same obligations as [`take_owned_bytes`].
274#[must_use]
275pub unsafe fn take_owned_text(buffer: AlefOwnedBuffer) -> String {
276    // SAFETY: upheld by this function's caller contract.
277    String::from_utf8_lossy(&unsafe { take_owned_bytes(buffer) }).into_owned()
278}
279
280unsafe fn take_owned_buffer(buffer: AlefOwnedBuffer) -> String {
281    let message = if buffer.ptr.is_null() {
282        if buffer.len == 0 {
283            String::new()
284        } else {
285            "component returned an invalid error buffer".to_string()
286        }
287    } else {
288        String::from_utf8_lossy(unsafe { core::slice::from_raw_parts(buffer.ptr, buffer.len) }).into_owned()
289    };
290    if let Some(free) = buffer.free {
291        unsafe { free(buffer.context, buffer.ptr, buffer.len, buffer.capacity) };
292    }
293    message
294}
295
296fn contract_table_from_descriptor<T: AlefContract>(descriptor: &AlefComponentV1) -> Result<&T, ComponentError> {
297    if descriptor.contract_hash != T::CONTRACT_HASH
298        || descriptor.contract_size < core::mem::size_of::<T>()
299        || descriptor.contract.is_null()
300        || !(descriptor.contract as usize).is_multiple_of(core::mem::align_of::<T>())
301    {
302        return Err(ComponentError::InvalidContractTable);
303    }
304    Ok(unsafe { &*descriptor.contract.cast::<T>() })
305}
306
307fn process_pins() -> &'static Mutex<Vec<Arc<Library>>> {
308    static PINS: OnceLock<Mutex<Vec<Arc<Library>>>> = OnceLock::new();
309    PINS.get_or_init(|| Mutex::new(Vec::new()))
310}
311
312fn pin_for_process(library: Arc<Library>) {
313    process_pins()
314        .lock()
315        .unwrap_or_else(|poisoned| poisoned.into_inner())
316        .push(library);
317}
318
319pub(crate) fn validate_manifest(
320    manifest: &ComponentManifest,
321    requirements: &ComponentRequirements,
322) -> Result<(), ComponentError> {
323    if manifest.schema_version != COMPONENT_MANIFEST_SCHEMA || manifest.abi_version != COMPONENT_ABI_VERSION {
324        return Err(ComponentError::UnsupportedManifest);
325    }
326    if manifest.identity.component != requirements.component_id {
327        return Err(ComponentError::IdentityMismatch {
328            expected: requirements.component_id.clone(),
329            actual: manifest.identity.component.clone(),
330        });
331    }
332    let provided = manifest
333        .provides
334        .iter()
335        .find(|provided| provided.contract == requirements.contract_name)
336        .ok_or_else(|| ComponentError::ContractNotProvided {
337            component_id: requirements.component_id.clone(),
338            contract: requirements.contract_name.clone(),
339        })?;
340    if decode_hash(&provided.contract_hash)? != requirements.contract_hash {
341        return Err(ComponentError::ContractHashMismatch);
342    }
343    if let Some(expected) = requirements.feature_set_hash
344        && decode_hash(&manifest.identity.feature_hash)? != expected
345    {
346        return Err(ComponentError::FeatureSetHashMismatch);
347    }
348    Ok(())
349}
350
351// `struct_size` must be a descriptor's first field: only that guarantees a raw read of it
352// never touches memory beyond what the component actually allocated, even when the
353// allocation is shorter than `size_of::<AlefComponentV1>()`. If this ever fails, the raw
354// read in `reject_undersized_descriptor` is no longer sound and must be revisited.
355const _: () = assert!(core::mem::offset_of!(AlefComponentV1, struct_size) == 0);
356
357/// Read a just-returned descriptor's `struct_size` and reject it while it is still smaller
358/// than the current ABI, before a `&AlefComponentV1` reference is formed over it.
359///
360/// # Safety
361///
362/// `descriptor` must be non-null and point to memory valid for reads of at least
363/// `size_of::<usize>()` bytes at `AlefComponentV1`'s alignment; no wider reference to it may
364/// have been formed yet. Both are upheld by [`LoadedComponent::load`]'s caller contract: a
365/// component's entry point must return a pointer to at least a valid `struct_size` field.
366unsafe fn reject_undersized_descriptor(descriptor: NonNull<AlefComponentV1>) -> Result<(), ComponentError> {
367    // SAFETY: upheld by this function's caller contract; `struct_size` sits at offset 0
368    // (asserted above), so this read never reaches past the caller-guaranteed
369    // `size_of::<usize>()` bytes regardless of how large the real descriptor is.
370    let struct_size = unsafe { descriptor.as_ptr().cast::<usize>().read_unaligned() };
371    if struct_size < core::mem::size_of::<AlefComponentV1>() {
372        return Err(ComponentError::InvalidDescriptor("descriptor is smaller than ABI v1"));
373    }
374    Ok(())
375}
376
377unsafe fn validate_descriptor(
378    descriptor: &AlefComponentV1,
379    requirements: &ComponentRequirements,
380) -> Result<(String, String), ComponentError> {
381    if descriptor.struct_size < core::mem::size_of::<AlefComponentV1>() {
382        return Err(ComponentError::InvalidDescriptor("descriptor is smaller than ABI v1"));
383    }
384    if descriptor.abi_major != ABI_MAJOR_V1 || descriptor.abi_minor > ABI_MINOR_V1 {
385        return Err(ComponentError::IncompatibleAbi {
386            expected_major: ABI_MAJOR_V1,
387            expected_minor: ABI_MINOR_V1,
388            actual_major: descriptor.abi_major,
389            actual_minor: descriptor.abi_minor,
390        });
391    }
392    let component_id = unsafe { copy_abi_str(descriptor.component_id)? };
393    let component_version = unsafe { copy_abi_str(descriptor.component_version)? };
394    if component_id != requirements.component_id {
395        return Err(ComponentError::IdentityMismatch {
396            expected: requirements.component_id.clone(),
397            actual: component_id,
398        });
399    }
400    if descriptor.contract_hash != requirements.contract_hash {
401        return Err(ComponentError::ContractHashMismatch);
402    }
403    if requirements
404        .feature_set_hash
405        .is_some_and(|expected| descriptor.feature_set_hash != expected)
406    {
407        return Err(ComponentError::FeatureSetHashMismatch);
408    }
409    if descriptor.contract_size > 0 && descriptor.contract.is_null() {
410        return Err(ComponentError::InvalidDescriptor(
411            "non-empty contract has a null pointer",
412        ));
413    }
414    Ok((component_id, component_version))
415}
416
417unsafe fn copy_abi_str(value: alef_component_abi::AlefStr) -> Result<String, ComponentError> {
418    if value.len > MAX_DESCRIPTOR_STRING {
419        return Err(ComponentError::StringTooLong);
420    }
421    if value.len == 0 {
422        return Ok(String::new());
423    }
424    if value.ptr.is_null() {
425        return Err(ComponentError::InvalidDescriptor("non-empty string has a null pointer"));
426    }
427    let bytes = unsafe { core::slice::from_raw_parts(value.ptr.cast::<u8>(), value.len) };
428    let text = core::str::from_utf8(bytes).map_err(|_| ComponentError::InvalidUtf8)?;
429    Ok(text.to_owned())
430}
431
432pub(crate) fn decode_hash(value: &str) -> Result<[u8; 32], ComponentError> {
433    let bytes = hex::decode(value).map_err(|_| ComponentError::InvalidDigest(value.to_owned()))?;
434    bytes
435        .try_into()
436        .map_err(|_| ComponentError::InvalidDigest(value.to_owned()))
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use alef_component_abi::{AlefComponentV1, AlefStr};
443    use std::ffi::c_char;
444
445    static ID: &[u8] = b"demo";
446    static VERSION: &[u8] = b"1.2.3";
447
448    #[repr(C)]
449    struct DemoContract {
450        answer: u32,
451    }
452
453    unsafe impl AlefContract for DemoContract {
454        const CONTRACT_HASH: [u8; 32] = [7; 32];
455    }
456
457    static CONTRACT: DemoContract = DemoContract { answer: 42 };
458
459    fn descriptor(contract_hash: [u8; 32]) -> AlefComponentV1 {
460        AlefComponentV1 {
461            struct_size: core::mem::size_of::<AlefComponentV1>(),
462            abi_major: ABI_MAJOR_V1,
463            abi_minor: ABI_MINOR_V1,
464            component_id: AlefStr {
465                ptr: ID.as_ptr().cast::<c_char>(),
466                len: ID.len(),
467            },
468            component_version: AlefStr {
469                ptr: VERSION.as_ptr().cast::<c_char>(),
470                len: VERSION.len(),
471            },
472            contract_hash,
473            feature_set_hash: [8; 32],
474            contract: core::ptr::dangling(),
475            contract_size: 1,
476            create: None,
477            destroy: None,
478        }
479    }
480
481    fn requirements() -> ComponentRequirements {
482        ComponentRequirements {
483            component_id: "demo".into(),
484            contract_name: "engine".into(),
485            contract_hash: [7; 32],
486            feature_set_hash: Some([8; 32]),
487        }
488    }
489
490    #[test]
491    fn accepts_matching_descriptor() {
492        let descriptor = descriptor([7; 32]);
493        let validated = unsafe { validate_descriptor(&descriptor, &requirements()) }.unwrap();
494        assert_eq!(validated, ("demo".into(), "1.2.3".into()));
495    }
496
497    #[test]
498    fn rejects_wrong_contract_hash() {
499        let descriptor = descriptor([9; 32]);
500        assert!(matches!(
501            unsafe { validate_descriptor(&descriptor, &requirements()) },
502            Err(ComponentError::ContractHashMismatch)
503        ));
504    }
505
506    #[test]
507    fn rejects_short_descriptor_before_optional_fields_are_read() {
508        let mut descriptor = descriptor([7; 32]);
509        descriptor.struct_size = 8;
510        assert!(matches!(
511            unsafe { validate_descriptor(&descriptor, &requirements()) },
512            Err(ComponentError::InvalidDescriptor(_))
513        ));
514    }
515
516    /// Regression for forming a `&AlefComponentV1` over memory that only actually holds a
517    /// `struct_size` field. The backing allocation below is deliberately sized for nothing
518    /// more than that one `usize`, so `reject_undersized_descriptor`'s raw pointer read must
519    /// be the only access that happens -- ever forming a full `&AlefComponentV1` reference
520    /// over it would read past the allocation.
521    #[test]
522    fn rejects_a_descriptor_backed_by_memory_too_small_to_hold_the_full_struct() {
523        let layout = std::alloc::Layout::from_size_align(
524            core::mem::size_of::<usize>(),
525            core::mem::align_of::<AlefComponentV1>(),
526        )
527        .unwrap();
528        // SAFETY: `layout` has non-zero size.
529        let raw = unsafe { std::alloc::alloc(layout) };
530        assert!(!raw.is_null(), "test allocation failed");
531        // SAFETY: `raw` is valid for `size_of::<usize>()` bytes per `layout` and is
532        // sufficiently aligned for a `usize` write.
533        unsafe { raw.cast::<usize>().write_unaligned(8) };
534        let descriptor = NonNull::new(raw).unwrap().cast::<AlefComponentV1>();
535
536        // SAFETY: `descriptor` points to `size_of::<usize>()` valid bytes, matching this
537        // function's caller contract; no wider reference is formed here.
538        let result = unsafe { reject_undersized_descriptor(descriptor) };
539
540        // SAFETY: `raw`/`layout` match the earlier allocation exactly.
541        unsafe { std::alloc::dealloc(raw, layout) };
542        assert!(matches!(result, Err(ComponentError::InvalidDescriptor(_))));
543    }
544
545    #[test]
546    fn manifest_validation_checks_feature_identity() {
547        let manifest = ComponentManifest {
548            schema_version: COMPONENT_MANIFEST_SCHEMA,
549            abi_version: 1,
550            identity: crate::ComponentIdentity {
551                crate_name: "demo-core".into(),
552                component: "demo".into(),
553                version: "1.2.3".into(),
554                target: "test-target".into(),
555                feature_hash: hex::encode([9; 32]),
556                contract_hash: hex::encode([7; 32]),
557            },
558            provides: vec![crate::ComponentProvidedContract {
559                contract: "engine".into(),
560                interface_version: 1,
561                contract_hash: hex::encode([7; 32]),
562                implementation: "demo::Engine".into(),
563            }],
564            features: Vec::new(),
565            default_features: false,
566            library: crate::ComponentLibrary {
567                file: "libdemo.so".into(),
568                sha256: hex::encode([0; 32]),
569                size: 0,
570            },
571        };
572        assert!(matches!(
573            validate_manifest(&manifest, &requirements()),
574            Err(ComponentError::FeatureSetHashMismatch)
575        ));
576    }
577
578    #[test]
579    fn typed_contract_table_checks_hash_size_and_alignment() {
580        let mut descriptor = descriptor(DemoContract::CONTRACT_HASH);
581        descriptor.contract = (&raw const CONTRACT).cast();
582        descriptor.contract_size = core::mem::size_of::<DemoContract>();
583        assert_eq!(
584            contract_table_from_descriptor::<DemoContract>(&descriptor)
585                .unwrap()
586                .answer,
587            42
588        );
589
590        descriptor.contract_hash = [9; 32];
591        assert!(matches!(
592            contract_table_from_descriptor::<DemoContract>(&descriptor),
593            Err(ComponentError::InvalidContractTable)
594        ));
595    }
596}