Skip to main content

nitro_cli/enclave_proc/
resource_manager.rs

1// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3#![deny(missing_docs)]
4#![deny(warnings)]
5#![allow(unknown_lints)]
6#![allow(deref_nullptr)]
7
8use aws_nitro_enclaves_image_format::defs::EifIdentityInfo;
9use driver_bindings::*;
10use eif_loader::{enclave_ready, TIMEOUT_MINUTE_MS};
11use libc::c_int;
12use log::{debug, info};
13use std::collections::BTreeMap;
14use std::fs::{File, OpenOptions};
15use std::io::prelude::*;
16use std::io::Error;
17use std::mem::size_of;
18use std::os::unix::io::{AsRawFd, RawFd};
19use std::str;
20use std::sync::{Arc, Mutex};
21use std::time::Duration;
22use vsock::{VsockAddr, VsockListener};
23
24use crate::common::json_output::EnclaveBuildInfo;
25use crate::common::{construct_error_message, notify_error};
26use crate::common::{
27    ExitGracefully, NitroCliErrorEnum, NitroCliFailure, NitroCliResult, ENCLAVE_READY_VSOCK_PORT,
28    VMADDR_CID_PARENT,
29};
30use crate::enclave_proc::connection::Connection;
31use crate::enclave_proc::connection::{safe_conn_eprintln, safe_conn_println};
32use crate::enclave_proc::cpu_info::EnclaveCpuConfig;
33use crate::enclave_proc::utils::get_run_enclaves_info;
34use crate::enclave_proc::utils::{GiB, MiB};
35use crate::new_nitro_cli_failure;
36use crate::utils::ceil_div;
37
38/// CamelCase alias for the bindgen generated driver struct (ne_enclave_start_info).
39pub type EnclaveStartInfo = ne_enclave_start_info;
40
41/// CamelCase alias for the bindgen generated driver struct (ne_user_memory_region).
42pub type UserMemoryRegion = ne_user_memory_region;
43
44/// CamelCase alias for the bindgen generate struct (ne_image_load_info).
45pub type ImageLoadInfo = ne_image_load_info;
46
47/// The internal data type needed for describing an enclave.
48type UnpackedHandle = (u64, u64, u64, Vec<u32>, u64, u64, EnclaveState);
49
50/// The bit indicating if an enclave has been launched in debug mode.
51pub const NE_ENCLAVE_DEBUG_MODE: u64 = 0x1;
52
53/// Constant number used for computing the lower memory limit.
54const ENCLAVE_MEMORY_EIF_SIZE_RATIO: u64 = 4;
55
56/// Enclave Image Format (EIF) flag.
57const NE_EIF_IMAGE: u64 = 0x01;
58
59/// Flag indicating a memory region for enclave general usage.
60const NE_DEFAULT_MEMORY_REGION: u64 = 0;
61
62/// Magic number for Nitro Enclave IOCTL codes.
63const NE_MAGIC: u64 = 0xAE;
64
65/// Path corresponding to the Nitro Enclaves device file.
66const NE_DEV_FILEPATH: &str = "/dev/nitro_enclaves";
67
68/// IOCTL code for `NE_CREATE_VM`.
69pub const NE_CREATE_VM: u64 = nix::request_code_read!(NE_MAGIC, 0x20, size_of::<u64>()) as _;
70
71/// IOCTL code for `NE_ADD_VCPU`.
72pub const NE_ADD_VCPU: u64 = nix::request_code_readwrite!(NE_MAGIC, 0x21, size_of::<u32>()) as _;
73
74/// IOCTL code for `NE_GET_IMAGE_LOAD_INFO`.
75pub const NE_GET_IMAGE_LOAD_INFO: u64 =
76    nix::request_code_readwrite!(NE_MAGIC, 0x22, size_of::<ImageLoadInfo>()) as _;
77
78/// IOCTL code for `NE_SET_USER_MEMORY_REGION`.
79pub const NE_SET_USER_MEMORY_REGION: u64 =
80    nix::request_code_write!(NE_MAGIC, 0x23, size_of::<MemoryRegion>()) as _;
81
82/// IOCTL code for `NE_START_ENCLAVE`.
83pub const NE_START_ENCLAVE: u64 =
84    nix::request_code_readwrite!(NE_MAGIC, 0x24, size_of::<EnclaveStartInfo>()) as _;
85
86/// Mapping between hugepage size and allocation flag, in descending order of size.
87const HUGE_PAGE_MAP: [(libc::c_int, u64); 9] = [
88    (libc::MAP_HUGE_16GB, 16 * GiB),
89    (libc::MAP_HUGE_2GB, 2 * GiB),
90    (libc::MAP_HUGE_1GB, GiB),
91    (libc::MAP_HUGE_512MB, 512 * MiB),
92    (libc::MAP_HUGE_256MB, 256 * MiB),
93    (libc::MAP_HUGE_32MB, 32 * MiB),
94    (libc::MAP_HUGE_16MB, 16 * MiB),
95    (libc::MAP_HUGE_8MB, 8 * MiB),
96    (libc::MAP_HUGE_2MB, 2 * MiB),
97];
98
99/// A memory region used by the enclave memory allocator.
100#[derive(Clone, Debug)]
101pub struct MemoryRegion {
102    /// Flags to determine the usage for the memory region.
103    flags: u64,
104    /// The region's size in bytes.
105    mem_size: u64,
106    /// The region's virtual address.
107    mem_addr: u64,
108}
109
110/// The state an enclave may be in.
111#[derive(Clone, Default)]
112pub enum EnclaveState {
113    #[default]
114    /// The enclave is not running (it's either not started or has been terminated).
115    Empty,
116    /// The enclave is running.
117    Running,
118    /// The enclave is in the process of terminating.
119    Terminating,
120}
121
122/// Helper structure to allocate memory resources needed by an enclave.
123#[derive(Clone, Default)]
124struct ResourceAllocator {
125    /// The requested memory size in bytes.
126    requested_mem: u64,
127    /// The memory regions that have actually been allocated.
128    mem_regions: Vec<MemoryRegion>,
129}
130
131/// Helper structure for managing an enclave's resources.
132#[derive(Default)]
133struct EnclaveHandle {
134    /// The CPU configuration as requested by the user.
135    #[allow(dead_code)]
136    cpu_config: EnclaveCpuConfig,
137    /// List of CPU IDs provided to the enclave.
138    cpu_ids: Vec<u32>,
139    /// Amount of memory allocated for the enclave, in MB.
140    allocated_memory_mib: u64,
141    /// The enclave slot ID.
142    slot_uid: u64,
143    /// The enclave CID.
144    enclave_cid: Option<u64>,
145    /// Enclave flags (including the enclave debug mode flag).
146    flags: u64,
147    /// The driver-provided enclave descriptor.
148    enc_fd: RawFd,
149    /// The allocator used to manage enclave memory.
150    resource_allocator: ResourceAllocator,
151    /// The enclave image file.
152    eif_file: Option<File>,
153    /// The current state the enclave is in.
154    state: EnclaveState,
155    /// PCR values.
156    build_info: EnclaveBuildInfo,
157    /// EIF metadata
158    metadata: Option<EifIdentityInfo>,
159}
160
161/// The structure which manages an enclave in a thread-safe manner.
162#[derive(Clone, Default)]
163pub struct EnclaveManager {
164    /// The full ID of the managed enclave.
165    pub enclave_id: String,
166    /// Name of the managed enclave.
167    pub enclave_name: String,
168    /// A thread-safe handle to the enclave's resources.
169    enclave_handle: Arc<Mutex<EnclaveHandle>>,
170}
171
172impl std::fmt::Display for EnclaveState {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        match self {
175            EnclaveState::Empty => write!(f, "EMPTY"),
176            EnclaveState::Running => write!(f, "RUNNING"),
177            EnclaveState::Terminating => write!(f, "TERMINATING"),
178        }
179    }
180}
181
182impl Default for EnclaveBuildInfo {
183    fn default() -> Self {
184        EnclaveBuildInfo::new(BTreeMap::new())
185    }
186}
187
188/// Construct a UserMemoryRegion object from a MemoryRegion instance.
189/// Implementing the `From` trait automatically gives access to an
190/// implementation of `Into` which can be used for a MemoryRegion instance.
191impl From<&MemoryRegion> for UserMemoryRegion {
192    fn from(mem_reg: &MemoryRegion) -> UserMemoryRegion {
193        UserMemoryRegion {
194            flags: mem_reg.flags,
195            memory_size: mem_reg.mem_size,
196            userspace_addr: mem_reg.mem_addr,
197        }
198    }
199}
200
201impl MemoryRegion {
202    /// Create a new `MemoryRegion` instance with the specified size (in bytes).
203    pub fn new(hugepage_flag: libc::c_int) -> NitroCliResult<Self> {
204        let region_index = HUGE_PAGE_MAP
205            .iter()
206            .position(|&page_info| page_info.0 == hugepage_flag)
207            .ok_or_else(|| {
208                new_nitro_cli_failure!(
209                    &format!("Failed to find huge page entry for flag {hugepage_flag:X?}"),
210                    NitroCliErrorEnum::NoSuchHugepageFlag
211                )
212            })?;
213        let region_size = HUGE_PAGE_MAP[region_index].1;
214
215        let addr = unsafe {
216            libc::mmap(
217                std::ptr::null_mut(),
218                region_size as usize,
219                libc::PROT_READ | libc::PROT_WRITE,
220                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_HUGETLB | hugepage_flag,
221                -1,
222                0,
223            )
224        };
225
226        if addr == libc::MAP_FAILED {
227            return Err(new_nitro_cli_failure!(
228                "Failed to map memory",
229                NitroCliErrorEnum::EnclaveMmapError
230            ));
231        }
232
233        // Record the allocated region.
234        Ok(MemoryRegion {
235            flags: NE_DEFAULT_MEMORY_REGION,
236            mem_size: region_size,
237            mem_addr: addr as u64,
238        })
239    }
240
241    /// Create a new `MemoryRegion` instance with the specified values.
242    pub fn new_with(flags: u64, mem_addr: u64, mem_size: u64) -> Self {
243        MemoryRegion {
244            flags,
245            mem_size,
246            mem_addr,
247        }
248    }
249
250    /// Free the memory region, if it has been allocated earlier.
251    fn free(&mut self) -> NitroCliResult<()> {
252        // Do nothing if the region has already been freed.
253        if self.mem_addr == 0 {
254            return Ok(());
255        }
256
257        let rc =
258            unsafe { libc::munmap(self.mem_addr as *mut libc::c_void, self.mem_size as usize) };
259
260        if rc < 0 {
261            return Err(new_nitro_cli_failure!(
262                "Failed to unmap memory",
263                NitroCliErrorEnum::EnclaveMunmapError
264            ));
265        }
266
267        // Set the address and length to 0 to avoid double-freeing.
268        self.mem_addr = 0;
269        self.mem_size = 0;
270
271        Ok(())
272    }
273
274    /// Write the content from a file into memory at a given offset.
275    fn fill_from_file(
276        &self,
277        file: &mut File,
278        region_offset: usize,
279        size: usize,
280    ) -> NitroCliResult<()> {
281        let offset_plus_size = region_offset.checked_add(size).ok_or_else(|| {
282            new_nitro_cli_failure!(
283                "Memory overflow when writing EIF file to region",
284                NitroCliErrorEnum::MemoryOverflow
285            )
286        })?;
287
288        if offset_plus_size > self.mem_size as usize {
289            return Err(new_nitro_cli_failure!(
290                "Out of region",
291                NitroCliErrorEnum::MemoryOverflow
292            ));
293        }
294
295        let bytes = unsafe {
296            std::slice::from_raw_parts_mut(self.mem_addr as *mut u8, self.mem_size as usize)
297        };
298
299        file.read_exact(&mut bytes[region_offset..region_offset + size])
300            .map_err(|e| {
301                new_nitro_cli_failure!(
302                    &format!("Error while reading from enclave image: {e:?}"),
303                    NitroCliErrorEnum::EifParsingError
304                )
305            })?;
306
307        Ok(())
308    }
309
310    /// Get the virtual address of the memory region.
311    pub fn mem_addr(&self) -> u64 {
312        self.mem_addr
313    }
314
315    /// Get the size in bytes of the memory region.
316    pub fn mem_size(&self) -> u64 {
317        self.mem_size
318    }
319}
320
321impl Drop for MemoryRegion {
322    fn drop(&mut self) {
323        self.free()
324            .ok_or_exit_with_errno(Some("Failed to drop memory region"));
325    }
326}
327
328impl ResourceAllocator {
329    /// Create a new `ResourceAllocator` instance which must cover at least the requested amount of memory (in bytes).
330    fn new(requested_mem: u64) -> NitroCliResult<Self> {
331        if requested_mem == 0 {
332            return Err(new_nitro_cli_failure!(
333                "Cannot start an enclave with no memory",
334                NitroCliErrorEnum::InsufficientMemoryRequested
335            )
336            .add_info(vec!["memory", &(requested_mem >> 20).to_string()]));
337        }
338
339        Ok(ResourceAllocator {
340            requested_mem,
341            mem_regions: Vec::new(),
342        })
343    }
344
345    /// Allocate and provide a list of memory regions. This function creates a list of
346    /// memory regions which contain at least `self.requested_mem` bytes. Each region
347    /// is equivalent to a huge-page and is allocated using memory mapping.
348    fn allocate(&mut self) -> NitroCliResult<&Vec<MemoryRegion>> {
349        let mut allocated_pages = BTreeMap::<u64, u32>::new();
350        let mut needed_mem = self.requested_mem as i64;
351        let mut split_index = 0;
352
353        info!(
354            "Allocating memory regions to hold {} bytes.",
355            self.requested_mem
356        );
357
358        // Always allocate larger pages first, to reduce fragmentation and page count.
359        // Once an allocation of a given page size fails, proceed to the next smaller
360        // page size and retry.
361        for page_info in HUGE_PAGE_MAP.iter() {
362            while needed_mem >= page_info.1 as i64 {
363                match MemoryRegion::new(page_info.0) {
364                    Ok(value) => {
365                        needed_mem -= value.mem_size as i64;
366                        self.mem_regions.push(value);
367                    }
368                    Err(_) => break,
369                }
370            }
371        }
372
373        // If the user requested exactly the amount of memory that was reserved earlier,
374        // we should be left with no more memory that needs allocation. But if the user
375        // requests a smaller amount, we must then aim to reduce waster memory from
376        // larger-page allocations (Ex: if we have 1 x 1 GB page and 1 x 2 MB page, but
377        // we want to allocate only 512 MB, the above algorithm will have allocated only
378        // the 2 MB page, since the 1 GB page was too large for what was needed; we now
379        // need to allocate in increasing order of page size in order to reduce wastage).
380
381        if needed_mem > 0 {
382            for page_info in HUGE_PAGE_MAP.iter().rev() {
383                while needed_mem > 0 {
384                    match MemoryRegion::new(page_info.0) {
385                        Ok(value) => {
386                            needed_mem -= value.mem_size as i64;
387                            self.mem_regions.push(value);
388                        }
389                        Err(_) => break,
390                    }
391                }
392            }
393        }
394
395        // If we still have memory to allocate, it means we have insufficient resources.
396        if needed_mem > 0 {
397            return Err(new_nitro_cli_failure!(
398                &format!(
399                    "Failed to allocate entire memory ({} MB remained)",
400                    needed_mem >> 20
401                ),
402                NitroCliErrorEnum::InsufficientMemoryAvailable
403            )
404            .add_info(vec!["memory", &(self.requested_mem >> 20).to_string()]));
405        }
406
407        // At this point, we may have allocated more than we need, so we release all
408        // regions we no longer need, starting with the smallest ones.
409        self.mem_regions
410            .sort_by_key(|region| std::cmp::Reverse(region.mem_size));
411
412        needed_mem = self.requested_mem as i64;
413        for region in self.mem_regions.iter() {
414            if needed_mem <= 0 {
415                break;
416            }
417
418            needed_mem -= region.mem_size as i64;
419            split_index += 1
420        }
421
422        // The regions that we no longer need are freed automatically on draining, since
423        // MemRegion implements Drop.
424        self.mem_regions.drain(split_index..);
425
426        // Generate a summary of the allocated memory.
427        for region in self.mem_regions.iter() {
428            if let Some(page_count) = allocated_pages.get_mut(&region.mem_size) {
429                *page_count += 1;
430            } else {
431                allocated_pages.insert(region.mem_size, 1);
432            }
433        }
434
435        info!(
436            "Allocated {} region(s): {}",
437            self.mem_regions.len(),
438            allocated_pages
439                .iter()
440                .map(|(size, count)| format!("{} page(s) of {} MB", count, size >> 20))
441                .collect::<Vec<String>>()
442                .join(", ")
443        );
444
445        Ok(&self.mem_regions)
446    }
447
448    /// Free all previously-allocated memory regions.
449    fn free(&mut self) -> NitroCliResult<()> {
450        for region in self.mem_regions.iter_mut() {
451            region
452                .free()
453                .map_err(|e| e.add_subaction("Failed to free enclave memory region".to_string()))?;
454        }
455
456        self.mem_regions.clear();
457        Ok(())
458    }
459}
460
461impl Drop for ResourceAllocator {
462    fn drop(&mut self) {
463        self.free()
464            .ok_or_exit_with_errno(Some("Failed to drop resource allocator"));
465    }
466}
467
468impl EnclaveHandle {
469    /// Create a new enclave handle instance.
470    fn new(
471        enclave_cid: Option<u64>,
472        memory_mib: u64,
473        cpu_config: EnclaveCpuConfig,
474        eif_file: File,
475        debug_mode: bool,
476    ) -> NitroCliResult<Self> {
477        let requested_mem = memory_mib << 20;
478        let eif_size = eif_file
479            .metadata()
480            .map_err(|e| {
481                new_nitro_cli_failure!(
482                    &format!("Failed to get enclave image file metadata: {e:?}"),
483                    NitroCliErrorEnum::FileOperationFailure
484                )
485            })?
486            .len();
487        if ENCLAVE_MEMORY_EIF_SIZE_RATIO * eif_size > requested_mem {
488            return Err(new_nitro_cli_failure!(
489                &format!(
490                    "At least {} MB must be allocated (which is {} times the EIF file size)",
491                    ceil_div(ceil_div(eif_size, 1024), 1024) * ENCLAVE_MEMORY_EIF_SIZE_RATIO,
492                    ENCLAVE_MEMORY_EIF_SIZE_RATIO
493                ),
494                NitroCliErrorEnum::InsufficientMemoryRequested
495            )
496            .add_info(vec![
497                "memory",
498                &memory_mib.to_string(),
499                &(ceil_div(ceil_div(eif_size, 1024), 1024) * ENCLAVE_MEMORY_EIF_SIZE_RATIO)
500                    .to_string(),
501            ]));
502        }
503
504        // Open the device file.
505        let dev_file = OpenOptions::new()
506            .read(true)
507            .write(true)
508            .open(NE_DEV_FILEPATH)
509            .map_err(|e| {
510                new_nitro_cli_failure!(
511                    &format!("Failed to open device file: {e:?}"),
512                    NitroCliErrorEnum::FileOperationFailure
513                )
514                .add_info(vec![NE_DEV_FILEPATH, "Open"])
515            })?;
516
517        let mut slot_uid: u64 = 0;
518        let enc_fd = EnclaveHandle::do_ioctl(dev_file.as_raw_fd(), NE_CREATE_VM, &mut slot_uid)
519            .map_err(|e| e.add_subaction("Create VM ioctl failed".to_string()))?;
520        let flags: u64 = if debug_mode { NE_ENCLAVE_DEBUG_MODE } else { 0 };
521
522        if enc_fd < 0 {
523            return Err(new_nitro_cli_failure!(
524                &format!("Invalid enclave file descriptor ({enc_fd})"),
525                NitroCliErrorEnum::InvalidEnclaveFd
526            ));
527        }
528
529        Ok(EnclaveHandle {
530            cpu_config,
531            cpu_ids: vec![],
532            allocated_memory_mib: 0,
533            slot_uid,
534            enclave_cid,
535            flags,
536            enc_fd,
537            resource_allocator: ResourceAllocator::new(requested_mem)
538                .map_err(|e| e.add_subaction("Create resource allocator".to_string()))?,
539            eif_file: Some(eif_file),
540            state: EnclaveState::default(),
541            build_info: EnclaveBuildInfo::new(BTreeMap::new()),
542            metadata: None,
543        })
544    }
545
546    /// Initialize the enclave environment and start the enclave.
547    fn create_enclave(
548        &mut self,
549        enclave_name: String,
550        connection: Option<&Connection>,
551    ) -> NitroCliResult<String> {
552        self.init_memory(connection)
553            .map_err(|e| e.add_subaction("Memory initialization issue".to_string()))?;
554        self.init_cpus()
555            .map_err(|e| e.add_subaction("vCPUs initialization issue".to_string()))?;
556
557        let sockaddr = VsockAddr::new(VMADDR_CID_PARENT, ENCLAVE_READY_VSOCK_PORT);
558        let listener = VsockListener::bind(&sockaddr).map_err(|_| {
559            new_nitro_cli_failure!(
560                "Enclave boot heartbeat vsock connection - vsock bind error",
561                NitroCliErrorEnum::EnclaveBootFailure
562            )
563        })?;
564
565        let enclave_start = self
566            .start(connection)
567            .map_err(|e| e.add_subaction("Enclave start issue".to_string()))?;
568
569        // Get eif size to feed it to calculate_necessary_timeout helper function
570        let eif_size = self
571            .eif_file
572            .as_ref()
573            .ok_or_else(|| {
574                new_nitro_cli_failure!(
575                    "Failed to get EIF file",
576                    NitroCliErrorEnum::FileOperationFailure
577                )
578            })?
579            .metadata()
580            .map_err(|e| {
581                new_nitro_cli_failure!(
582                    &format!("Failed to get enclave image file metadata: {e:?}"),
583                    NitroCliErrorEnum::FileOperationFailure
584                )
585            })?
586            .len();
587
588        // Update the poll timeout based on the eif size or allocated memory
589        let poll_timeout = calculate_necessary_timeout(eif_size, self.allocated_memory_mib * MiB);
590
591        enclave_ready(listener, poll_timeout).map_err(|err| {
592            let err_msg = format!("Waiting on enclave to boot failed with error {err:?}");
593            self.terminate_enclave_error(&err_msg);
594            new_nitro_cli_failure!(&err_msg, NitroCliErrorEnum::EnclaveBootFailure)
595        })?;
596
597        self.enclave_cid = Some(enclave_start.enclave_cid);
598
599        let info = get_run_enclaves_info(
600            enclave_name,
601            enclave_start.enclave_cid,
602            self.slot_uid,
603            self.cpu_ids.clone(),
604            self.allocated_memory_mib,
605        )
606        .map_err(|e| e.add_subaction("Get RunEnclaves information issue".to_string()))?;
607
608        safe_conn_println(
609            connection,
610            serde_json::to_string_pretty(&info)
611                .map_err(|err| {
612                    new_nitro_cli_failure!(
613                        &format!("Failed to display RunEnclaves data: {err:?}"),
614                        NitroCliErrorEnum::SerdeError
615                    )
616                })?
617                .as_str(),
618        )?;
619
620        Ok(info.enclave_id)
621    }
622
623    /// Allocate memory and provide it to the enclave.
624    fn init_memory(&mut self, connection: Option<&Connection>) -> NitroCliResult<()> {
625        // Allocate the memory regions needed by the enclave.
626        safe_conn_eprintln(connection, "Start allocating memory...")?;
627
628        let requested_mem_mib = self.resource_allocator.requested_mem >> 20;
629        let regions = self
630            .resource_allocator
631            .allocate()
632            .map_err(|e| e.add_subaction("Failed to allocate enclave memory".to_string()))?;
633
634        self.allocated_memory_mib = regions.iter().fold(0, |mut acc, val| {
635            acc += val.mem_size;
636            acc
637        }) >> 20;
638
639        if self.allocated_memory_mib < requested_mem_mib {
640            return Err(new_nitro_cli_failure!(
641                &format!(
642                    "Failed to allocate sufficient memory (requested {} MB, but got {} MB)",
643                    requested_mem_mib, self.allocated_memory_mib
644                ),
645                NitroCliErrorEnum::InsufficientMemoryAvailable
646            )
647            .add_info(vec!["memory", &requested_mem_mib.to_string()]));
648        }
649
650        let eif_file = self.eif_file.as_mut().ok_or_else(|| {
651            new_nitro_cli_failure!(
652                "Failed to get mutable reference to EIF file",
653                NitroCliErrorEnum::FileOperationFailure
654            )
655        })?;
656
657        let mut image_load_info = ImageLoadInfo {
658            flags: NE_EIF_IMAGE,
659            memory_offset: 0,
660        };
661        EnclaveHandle::do_ioctl(self.enc_fd, NE_GET_IMAGE_LOAD_INFO, &mut image_load_info)
662            .map_err(|e| e.add_subaction("Get image load info ioctl failed".to_string()))?;
663
664        debug!("Memory load information: {:?}", image_load_info);
665        write_eif_to_regions(eif_file, regions, image_load_info.memory_offset as usize)
666            .map_err(|e| e.add_subaction("Write EIF to enclave memory regions".to_string()))?;
667
668        // Provide the regions to the driver for ownership change.
669        for region in regions {
670            let mut user_mem_region: UserMemoryRegion = region.into();
671            EnclaveHandle::do_ioctl(self.enc_fd, NE_SET_USER_MEMORY_REGION, &mut user_mem_region)
672                .map_err(|e| e.add_subaction("Set user memory region ioctl failed".to_string()))?;
673        }
674
675        info!("Finished initializing memory.");
676
677        Ok(())
678    }
679
680    /// Initialize a single vCPU from a given ID.
681    fn init_single_cpu(&mut self, mut cpu_id: u32) -> NitroCliResult<()> {
682        EnclaveHandle::do_ioctl(self.enc_fd, NE_ADD_VCPU, &mut cpu_id)
683            .map_err(|e| e.add_subaction("Add vCPU ioctl failed".to_string()))?;
684
685        self.cpu_ids.push(cpu_id);
686        debug!("Added CPU with ID {}.", cpu_id);
687
688        Ok(())
689    }
690
691    /// Provide CPUs from the parent instance to the enclave.
692    fn init_cpus(&mut self) -> NitroCliResult<()> {
693        let cpu_config = self.cpu_config.clone();
694
695        match cpu_config {
696            EnclaveCpuConfig::List(cpu_ids) => {
697                for cpu_id in cpu_ids {
698                    self.init_single_cpu(cpu_id).map_err(|e| {
699                        e.add_subaction(format!("Failed to add CPU with ID {cpu_id}"))
700                    })?;
701                }
702            }
703            EnclaveCpuConfig::Count(cpu_count) => {
704                for _ in 0..cpu_count {
705                    self.init_single_cpu(0)?;
706                }
707            }
708        }
709
710        Ok(())
711    }
712
713    /// Start an enclave after providing it with its necessary resources.
714    fn start(&mut self, connection: Option<&Connection>) -> NitroCliResult<EnclaveStartInfo> {
715        let mut start = EnclaveStartInfo {
716            flags: self.flags,
717            enclave_cid: self.enclave_cid.unwrap_or(0),
718        };
719
720        EnclaveHandle::do_ioctl(self.enc_fd, NE_START_ENCLAVE, &mut start)
721            .map_err(|e| e.add_subaction("Start enclave ioctl failed".to_string()))?;
722
723        safe_conn_eprintln(
724            connection,
725            format!(
726                "Started enclave with enclave-cid: {}, memory: {} MiB, cpu-ids: {:?}",
727                { start.enclave_cid },
728                self.allocated_memory_mib,
729                self.cpu_ids
730            )
731            .as_str(),
732        )?;
733
734        Ok(start)
735    }
736
737    /// Terminate an enclave.
738    fn terminate_enclave(&mut self) -> NitroCliResult<()> {
739        if self.enclave_cid.unwrap_or(0) != 0 {
740            release_enclave_descriptor(self.enc_fd)
741                .map_err(|e| e.add_subaction("Failed to release enclave descriptor".to_string()))?;
742
743            // Release used memory.
744            self.resource_allocator
745                .free()
746                .map_err(|e| e.add_subaction("Failed to release used memory".to_string()))?;
747            info!("Enclave terminated.");
748
749            // Mark enclave as terminated.
750            self.clear();
751        }
752
753        Ok(())
754    }
755
756    /// Terminate an enclave and notify in case of errors.
757    fn terminate_enclave_and_notify(&mut self) {
758        // Attempt to terminate the enclave we are holding.
759        if let Err(error_info) = self.terminate_enclave() {
760            let mut err_msg = format!(
761                "Terminating enclave '{:X}' failed with error: {:?}",
762                self.slot_uid,
763                construct_error_message(&error_info).as_str()
764            );
765            err_msg.push_str(
766                "!!! The instance could be in an inconsistent state, please reboot it !!!",
767            );
768
769            // The error message should reach both the user and the logger.
770            notify_error(&err_msg);
771        }
772    }
773
774    /// Clear handle resources after terminating an enclave.
775    fn clear(&mut self) {
776        self.cpu_ids.clear();
777        self.allocated_memory_mib = 0;
778        self.enclave_cid = Some(0);
779        self.enc_fd = -1;
780        self.slot_uid = 0;
781    }
782
783    /// Terminate the enclave if `run-enclave` failed.
784    fn terminate_enclave_error(&mut self, err: &str) {
785        let err_msg = format!("{err}. Terminating the enclave...");
786
787        // Notify the user and the logger of the error, then terminate the enclave.
788        notify_error(&err_msg);
789        self.terminate_enclave_and_notify();
790    }
791
792    /// Wrapper over an `ioctl()` operation
793    fn do_ioctl<T>(fd: RawFd, ioctl_code: u64, arg: &mut T) -> NitroCliResult<i32> {
794        let rc = unsafe { libc::ioctl(fd, ioctl_code as _, arg) };
795        if rc >= 0 {
796            return Ok(rc);
797        }
798
799        let err_msg = match Error::last_os_error().raw_os_error().unwrap_or(0) as u32 {
800            NE_ERR_VCPU_ALREADY_USED => "The provided vCPU is already used".to_string(),
801            NE_ERR_VCPU_NOT_IN_CPU_POOL => {
802                "The provided vCPU is not available in the CPU pool".to_string()
803            }
804            NE_ERR_VCPU_INVALID_CPU_CORE => {
805                "The vCPU core ID is invalid for the CPU pool".to_string()
806            }
807            NE_ERR_INVALID_MEM_REGION_SIZE => {
808                "The memory region's size is not a multiple of 2 MiB".to_string()
809            }
810            NE_ERR_INVALID_MEM_REGION_ADDR => "The memory region's address is invalid".to_string(),
811            NE_ERR_UNALIGNED_MEM_REGION_ADDR => {
812                "The memory region's address is not aligned".to_string()
813            }
814            NE_ERR_MEM_REGION_ALREADY_USED => "The memory region is already used".to_string(),
815            NE_ERR_MEM_NOT_HUGE_PAGE => {
816                "The memory region is not backed by contiguous physical huge page(s)".to_string()
817            }
818            NE_ERR_MEM_DIFFERENT_NUMA_NODE => {
819                "The memory region's pages and the CPUs belong to different NUMA nodes".to_string()
820            }
821            NE_ERR_MEM_MAX_REGIONS => {
822                "The maximum number of memory regions per enclave has been reached".to_string()
823            }
824            NE_ERR_NO_MEM_REGIONS_ADDED => {
825                "The enclave cannot start because no memory regions have been added".to_string()
826            }
827            NE_ERR_NO_VCPUS_ADDED => {
828                "The enclave cannot start because no vCPUs have been added".to_string()
829            }
830            NE_ERR_ENCLAVE_MEM_MIN_SIZE => {
831                "The enclave's memory size is lower than the minimum supported".to_string()
832            }
833            NE_ERR_FULL_CORES_NOT_USED => {
834                "The enclave cannot start because full CPU cores have not been set".to_string()
835            }
836            NE_ERR_NOT_IN_INIT_STATE => {
837                "The enclave is in an incorrect state to set resources or start".to_string()
838            }
839            NE_ERR_INVALID_VCPU => {
840                "The provided vCPU is out of range of the available CPUs".to_string()
841            }
842            NE_ERR_NO_CPUS_AVAIL_IN_POOL => {
843                "The enclave cannot be created because no CPUs are available in the pool"
844                    .to_string()
845            }
846            NE_ERR_INVALID_PAGE_SIZE => {
847                "The memory region is not backed by page(s) multiple of 2 MiB".to_string()
848            }
849            NE_ERR_INVALID_FLAG_VALUE => {
850                "The provided flags value in the ioctl arg data structure is invalid".to_string()
851            }
852            NE_ERR_INVALID_ENCLAVE_CID => {
853                "The provided enclave CID is invalid, being a well-known CID or the parent VM CID"
854                    .to_string()
855            }
856            e => format!("An error has occurred: {e} (rc: {rc})"),
857        };
858
859        Err(new_nitro_cli_failure!(
860            &err_msg,
861            NitroCliErrorEnum::IoctlFailure
862        ))
863    }
864}
865
866impl Drop for EnclaveHandle {
867    fn drop(&mut self) {
868        // Check if we are (still) owning an enclave.
869        if self.enclave_cid.unwrap_or(0) == 0 {
870            debug!("Resource manager does not hold an enclave.");
871            return;
872        }
873
874        // Terminate the enclave, notifying of any errors.
875        self.terminate_enclave_and_notify();
876    }
877}
878
879impl EnclaveManager {
880    /// Create a new `EnclaveManager` instance.
881    pub fn new(
882        enclave_cid: Option<u64>,
883        memory_mib: u64,
884        cpu_ids: EnclaveCpuConfig,
885        eif_file: File,
886        debug_mode: bool,
887        enclave_name: String,
888    ) -> NitroCliResult<Self> {
889        let enclave_handle =
890            EnclaveHandle::new(enclave_cid, memory_mib, cpu_ids, eif_file, debug_mode)
891                .map_err(|e| e.add_subaction("Failed to create enclave handle".to_string()))?;
892        Ok(EnclaveManager {
893            enclave_id: String::new(),
894            enclave_name,
895            enclave_handle: Arc::new(Mutex::new(enclave_handle)),
896        })
897    }
898
899    /// Launch an enclave using the previously-set configuration.
900    ///
901    /// The enclave handle is locked throughout enclave creation. This is fine, since
902    /// the socket for receiving commands is exposed only after creation has completed.
903    pub fn run_enclave(&mut self, connection: Option<&Connection>) -> NitroCliResult<()> {
904        self.enclave_id = self
905            .enclave_handle
906            .lock()
907            .map_err(|e| {
908                new_nitro_cli_failure!(
909                    &format!("Failed to acquire lock: {e:?}"),
910                    NitroCliErrorEnum::LockAcquireFailure
911                )
912            })?
913            .create_enclave(self.enclave_name.clone(), connection)
914            .map_err(|e| e.add_subaction("Failed to create enclave".to_string()))?;
915        Ok(())
916    }
917
918    /// Set measurements field inside EnclaveHandle
919    pub fn set_measurements(
920        &mut self,
921        measurements: BTreeMap<String, String>,
922    ) -> NitroCliResult<()> {
923        self.enclave_handle
924            .lock()
925            .map_err(|e| {
926                new_nitro_cli_failure!(
927                    &format!("Failed to acquire lock: {e:?}"),
928                    NitroCliErrorEnum::LockAcquireFailure
929                )
930            })?
931            .build_info = EnclaveBuildInfo::new(measurements);
932        Ok(())
933    }
934
935    /// Set metadata field inside EnclaveHandle
936    pub fn set_metadata(&mut self, metadata: EifIdentityInfo) -> NitroCliResult<()> {
937        self.enclave_handle
938            .lock()
939            .map_err(|e| {
940                new_nitro_cli_failure!(
941                    &format!("Failed to acquire lock: {e:?}"),
942                    NitroCliErrorEnum::LockAcquireFailure
943                )
944            })?
945            .metadata = Some(metadata);
946        Ok(())
947    }
948
949    /// Get the resources needed for describing an enclave.
950    ///
951    /// The enclave handle is locked during this operation.
952    pub fn get_description_resources(&self) -> NitroCliResult<UnpackedHandle> {
953        let locked_handle = self.enclave_handle.lock().map_err(|e| {
954            new_nitro_cli_failure!(
955                &format!("Failed to acquire lock: {e:?}"),
956                NitroCliErrorEnum::LockAcquireFailure
957            )
958        })?;
959        Ok((
960            locked_handle.slot_uid,
961            locked_handle.enclave_cid.unwrap(),
962            locked_handle.cpu_ids.len() as u64,
963            locked_handle.cpu_ids.clone(),
964            locked_handle.allocated_memory_mib,
965            locked_handle.flags,
966            locked_handle.state.clone(),
967        ))
968    }
969
970    /// Get measurements from enclave handle
971    pub fn get_measurements(&self) -> NitroCliResult<EnclaveBuildInfo> {
972        let locked_handle = self.enclave_handle.lock().map_err(|e| {
973            new_nitro_cli_failure!(
974                &format!("Failed to acquire lock: {e:?}"),
975                NitroCliErrorEnum::LockAcquireFailure
976            )
977        })?;
978        Ok(locked_handle.build_info.clone())
979    }
980
981    /// Get metadata from enclave handle
982    pub fn get_metadata(&self) -> NitroCliResult<Option<EifIdentityInfo>> {
983        let locked_handle = self.enclave_handle.lock().map_err(|e| {
984            new_nitro_cli_failure!(
985                &format!("Failed to acquire lock: {e:?}"),
986                NitroCliErrorEnum::LockAcquireFailure
987            )
988        })?;
989        Ok(locked_handle.metadata.clone())
990    }
991
992    /// Get the resources (enclave CID) needed for connecting to the enclave console.
993    ///
994    /// The enclave handle is locked during this operation.
995    pub fn get_console_resources_enclave_cid(&self) -> NitroCliResult<u64> {
996        let locked_handle = self.enclave_handle.lock().map_err(|e| {
997            new_nitro_cli_failure!(
998                &format!("Failed to acquire lock: {e:?}"),
999                NitroCliErrorEnum::LockAcquireFailure
1000            )
1001        })?;
1002        Ok(locked_handle.enclave_cid.unwrap())
1003    }
1004
1005    /// Get the resources (enclave flags) needed for connecting to the enclave console.
1006    ///
1007    /// The enclave handle is locked during this operation.
1008    pub fn get_console_resources_enclave_flags(&self) -> NitroCliResult<u64> {
1009        let locked_handle = self.enclave_handle.lock().map_err(|e| {
1010            new_nitro_cli_failure!(
1011                &format!("Failed to acquire lock: {e:?}"),
1012                NitroCliErrorEnum::LockAcquireFailure
1013            )
1014        })?;
1015        Ok(locked_handle.flags)
1016    }
1017
1018    /// Get the resources needed for enclave termination.
1019    ///
1020    /// The enclave handle is locked during this operation.
1021    fn get_termination_resources(&self) -> NitroCliResult<(RawFd, ResourceAllocator)> {
1022        let locked_handle = self.enclave_handle.lock().map_err(|e| {
1023            new_nitro_cli_failure!(
1024                &format!("Failed to acquire lock: {e:?}"),
1025                NitroCliErrorEnum::LockAcquireFailure
1026            )
1027        })?;
1028        Ok((
1029            locked_handle.enc_fd,
1030            locked_handle.resource_allocator.clone(),
1031        ))
1032    }
1033
1034    /// Get the enclave descriptor.
1035    ///
1036    /// The enclave handle is locked during this operation.
1037    pub fn get_enclave_descriptor(&self) -> NitroCliResult<RawFd> {
1038        let locked_handle = self.enclave_handle.lock().map_err(|e| {
1039            new_nitro_cli_failure!(
1040                &format!("Failed to acquire lock: {e:?}"),
1041                NitroCliErrorEnum::LockAcquireFailure
1042            )
1043        })?;
1044        Ok(locked_handle.enc_fd)
1045    }
1046
1047    /// Update the state the enclave is in.
1048    ///
1049    /// The enclave handle is locked during this operation.
1050    pub fn update_state(&mut self, state: EnclaveState) -> NitroCliResult<()> {
1051        let mut locked_handle = self.enclave_handle.lock().map_err(|e| {
1052            new_nitro_cli_failure!(
1053                &format!("Failed to acquire lock: {e:?}"),
1054                NitroCliErrorEnum::LockAcquireFailure
1055            )
1056        })?;
1057        locked_handle.state = state;
1058        Ok(())
1059    }
1060
1061    /// Terminate the owned enclave.
1062    ///
1063    /// The enclave handle is locked only when getting the resources needed for termination.
1064    /// This will allow the enclave process to execute other commands while termination
1065    /// is taking place.
1066    pub fn terminate_enclave(&mut self) -> NitroCliResult<()> {
1067        let (enc_fd, mut resource_allocator) = self.get_termination_resources().map_err(|e| {
1068            e.add_subaction("Enclave manager failed to get termination resources".to_string())
1069        })?;
1070        release_enclave_descriptor(enc_fd).map_err(|e| {
1071            e.add_subaction("Enclave manager failed to release enclave descriptor".to_string())
1072        })?;
1073        resource_allocator.free().map_err(|e| {
1074            e.add_subaction("Enclave manager failed to free enclave memory".to_string())
1075        })?;
1076        self.enclave_handle
1077            .lock()
1078            .map_err(|e| {
1079                new_nitro_cli_failure!(
1080                    &format!("Failed to acquire lock: {e:?}"),
1081                    NitroCliErrorEnum::LockAcquireFailure
1082                )
1083            })?
1084            .clear();
1085        Ok(())
1086    }
1087}
1088
1089/// Write an enclave image file to the specified list of memory regions.
1090fn write_eif_to_regions(
1091    eif_file: &mut File,
1092    regions: &[MemoryRegion],
1093    image_write_offset: usize,
1094) -> NitroCliResult<()> {
1095    let file_size = eif_file
1096        .metadata()
1097        .map_err(|_| {
1098            new_nitro_cli_failure!(
1099                "Failed to obtain EIF file metadata",
1100                NitroCliErrorEnum::FileOperationFailure
1101            )
1102        })?
1103        .len() as usize;
1104
1105    eif_file.rewind().map_err(|_| {
1106        new_nitro_cli_failure!(
1107            "Failed to seek to the beginning of the EIF file",
1108            NitroCliErrorEnum::FileOperationFailure
1109        )
1110    })?;
1111
1112    let mut total_written: usize = 0;
1113
1114    for region in regions {
1115        let offset_plus_file_size = file_size.checked_add(image_write_offset).ok_or_else(|| {
1116            new_nitro_cli_failure!(
1117                "Memory overflow when trying to write EIF file",
1118                NitroCliErrorEnum::MemoryOverflow
1119            )
1120        })?;
1121
1122        if total_written >= offset_plus_file_size {
1123            // All bytes have been written.
1124            break;
1125        }
1126
1127        let written_plus_region_size = total_written
1128            .checked_add(region.mem_size as usize)
1129            .ok_or_else(|| {
1130                new_nitro_cli_failure!(
1131                    "Memory overflow when trying to write EIF file",
1132                    NitroCliErrorEnum::MemoryOverflow
1133                )
1134            })?;
1135
1136        if written_plus_region_size <= image_write_offset {
1137            // All bytes need to be skipped to get to the image write offset.
1138        } else {
1139            let region_offset = image_write_offset.saturating_sub(total_written);
1140            let file_offset = total_written.saturating_sub(image_write_offset);
1141            let size = std::cmp::min(
1142                region.mem_size as usize - region_offset,
1143                file_size - file_offset,
1144            );
1145            region
1146                .fill_from_file(eif_file, region_offset, size)
1147                .map_err(|e| {
1148                    e.add_subaction("Failed to fill region with file content".to_string())
1149                })?;
1150        }
1151        total_written += region.mem_size as usize;
1152    }
1153
1154    Ok(())
1155}
1156
1157/// Release the enclave descriptor.
1158fn release_enclave_descriptor(enc_fd: RawFd) -> NitroCliResult<()> {
1159    // Close enclave descriptor.
1160    let rc = unsafe { libc::close(enc_fd) };
1161    if rc < 0 {
1162        return Err(new_nitro_cli_failure!(
1163            "Failed to close enclave descriptor",
1164            NitroCliErrorEnum::FileOperationFailure
1165        ));
1166    }
1167
1168    Ok(())
1169}
1170
1171/// Check if the `NITRO_BETWEEN_PACKETS_MILLIS` environment variable is set.
1172/// If it is, return a `Duration` representing its value.
1173pub fn between_packets_delay() -> Option<Duration> {
1174    if let Ok(value) = std::env::var("NITRO_BETWEEN_PACKETS_MILLIS") {
1175        if let Ok(value) = value.parse::<u64>() {
1176            return Some(Duration::from_millis(value));
1177        }
1178    }
1179
1180    None
1181}
1182
1183/// Helper function which contains heuristic for enclave build timeout calculation
1184///
1185/// # Arguments
1186///
1187/// * `eif_size` - The EIF size in bytes
1188/// * `allocated_memory` - The memory size in bytes
1189///
1190/// # Examples
1191///
1192/// ```
1193/// use nitro_cli::enclave_proc::resource_manager::calculate_necessary_timeout;
1194/// use nitro_cli::enclave_proc::utils::GiB;
1195/// // Returns the timeout based on the 8GiB EIF size and 32GiB of allocated memory
1196/// let timeout = calculate_necessary_timeout(8 * GiB, 32 * GiB);
1197/// ```
1198pub fn calculate_necessary_timeout(eif_size: u64, allocated_memory: u64) -> c_int {
1199    // in case we have a valid eif_size give TIMEOUT_MINUTE_MS ms for each 6GiB
1200    let eif_size_timeout: c_int =
1201        ((1 + (eif_size - 1) / (6 * GiB)) as i32).saturating_mul(TIMEOUT_MINUTE_MS);
1202
1203    // Update the poll timeout to be TIMEOUT_MINUTE_MS per 100 GiB of enclave memory.
1204    let allocated_memory_timeout: c_int =
1205        ((1 + (allocated_memory - 1) / (100 * GiB)) as i32).saturating_mul(TIMEOUT_MINUTE_MS);
1206
1207    eif_size_timeout + allocated_memory_timeout
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212    use super::calculate_necessary_timeout;
1213    use crate::enclave_proc::utils::GiB;
1214    use eif_loader::TIMEOUT_MINUTE_MS;
1215
1216    #[test]
1217    fn test_timeout_calculation() {
1218        assert_eq!(
1219            calculate_necessary_timeout(2 * GiB, 32 * GiB),
1220            2 * TIMEOUT_MINUTE_MS
1221        );
1222        assert_eq!(
1223            calculate_necessary_timeout(6 * GiB, 32 * GiB),
1224            2 * TIMEOUT_MINUTE_MS
1225        );
1226        assert_eq!(
1227            calculate_necessary_timeout(10 * GiB, 32 * GiB),
1228            3 * TIMEOUT_MINUTE_MS
1229        );
1230
1231        assert_eq!(
1232            calculate_necessary_timeout(2 * GiB, 128 * GiB),
1233            3 * TIMEOUT_MINUTE_MS
1234        );
1235        assert_eq!(
1236            calculate_necessary_timeout(6 * GiB, 128 * GiB),
1237            3 * TIMEOUT_MINUTE_MS
1238        );
1239        assert_eq!(
1240            calculate_necessary_timeout(10 * GiB, 128 * GiB),
1241            4 * TIMEOUT_MINUTE_MS
1242        );
1243    }
1244}