Skip to main content

composefs_oci/
layer_transport.rs

1//! Abstraction layer for the OCI `GetLayer` producer side.
2//!
3//! Defines the [`LayerSource`] trait that decouples the fd-assembly and
4//! framing mechanics from the specific backing store (composefs repo today,
5//! containers-storage in a later step).  Both the in-process `copy` path and
6//! the varlink `GetLayer` handler use this trait.
7//!
8//! The shared entry point is [`serve_get_layer`], which:
9//! 1. Calls [`LayerSource::open`] to get real dirfds and a lifetime guard.
10//! 2. Calls [`composefs_splitdirfdstream::build_layer_fd_layout`] to place them
11//!    in the sparse layout, add dummies, keepalive pipe and extra lifetime fds.
12//! 3. Spawns the 3-phase self-reaping producer via
13//!    [`composefs_splitdirfdstream::spawn_self_reaping_producer`].
14//! 4. Splits the fd array into transport frames via
15//!    [`composefs_splitdirfdstream::split_fds_into_frames`].
16//! 5. Returns the ready frame batches + `dir_count` for the caller to wrap in
17//!    zlink `Reply` frames (interface-specific error types stay in the callers).
18//!
19//! # Dependency note
20//!
21//! This module lives in `composefs-oci` rather than `composefs-splitdirfdstream`
22//! because the trait references `anyhow::Result` and repository types, while
23//! `composefs-splitdirfdstream` must stay dependency-free of those crates.
24//! The cstor service (`composefs-storage`) CANNOT call `serve_get_layer`
25//! (it would create a dep cycle), so it calls `build_layer_fd_layout` +
26//! `spawn_self_reaping_producer` directly.
27
28use std::io::Write;
29use std::os::fd::{AsFd as _, OwnedFd};
30use std::sync::Arc;
31
32use anyhow::{Context as _, Result};
33
34use composefs::fsverity::FsVerityHashValue;
35use composefs::repository::Repository;
36use composefs_splitdirfdstream::{
37    FdLimitError, build_layer_fd_layout, spawn_self_reaping_producer, split_fds_into_frames,
38};
39
40use crate::layer_sync::produce_layer_splitdirfdstream;
41
42// ── LayerSource trait ─────────────────────────────────────────────────────────
43
44/// A producer that can open the real diff-directory file descriptors for one
45/// layer and stream the layer content as a `splitdirfdstream`.
46///
47/// Implementors supply two things:
48/// 1. The set of real diff-directory fds (in chain order) **plus an opaque
49///    lifetime guard** that must be held open until the consumer signals
50///    completion (keepalive-pipe EOF).  For the repo case the guard is `()`;
51///    for containers-storage the guard is the shared `LayerStoreLock`.
52/// 2. A function that, given the slot-index assignments from the sparse layout,
53///    produces the `splitdirfdstream` bytes into a writer.
54///
55/// The trait is object-safe so it can be stored in `Box<dyn LayerSource>`.
56pub trait LayerSource: Send + 'static {
57    /// Open the real diff-directory file descriptors for this layer, in chain order,
58    /// AND acquire any lifetime-bound resources (e.g. a shared layer-store lock).
59    ///
60    /// Returns `(dirfds, guard)` where:
61    /// - `dirfds` — the real diff-dir fds, one per chain layer, in chain order.
62    /// - `guard` — an opaque object that **must be held open** until the
63    ///   consumer finishes processing (keepalive-pipe EOF). Dropping the guard
64    ///   signals that the layer's storage may be released.  For the repo source
65    ///   this is `Box::new(())` (no-op).
66    ///
67    /// The lock is acquired BEFORE the fds are opened so that the lock →
68    /// open sequence is atomic (avoids a TOCTOU race where a concurrent
69    /// `podman rmi` could unlink a diff directory between lock and open).
70    ///
71    /// Called once per `GetLayer` request, before the sparse layout is built.
72    fn open(&self) -> Result<(Vec<OwnedFd>, Box<dyn Send + 'static>)>;
73
74    /// Write the layer content as a `splitdirfdstream` to `out`.
75    ///
76    /// `dirfd_index_map[i]` is the slot index within the sparse dirfds region
77    /// where the `i`-th real diff-dir fd was placed.  The producer must use
78    /// `write_file_backed_data(dirfd_index_map[i], ...)` for every file that
79    /// belongs to chain layer `i`.
80    ///
81    /// This method is called from a `spawn_blocking` context so it MAY block.
82    fn produce(&self, dirfd_index_map: &[u32], out: Box<dyn Write + Send>) -> Result<()>;
83}
84
85// ── RepoLayerSource ───────────────────────────────────────────────────────────
86
87/// [`LayerSource`] implementation backed by a composefs [`Repository`].
88///
89/// The objects directory is the single real diff-dir (chain index 0).
90pub struct RepoLayerSource<ObjectID: FsVerityHashValue> {
91    /// The repository from which to serve the layer.
92    pub repo: Arc<Repository<ObjectID>>,
93    /// fs-verity hash of the layer splitstream to serve.
94    pub layer_verity: ObjectID,
95}
96
97impl<ObjectID> std::fmt::Debug for RepoLayerSource<ObjectID>
98where
99    ObjectID: FsVerityHashValue + std::fmt::Debug,
100{
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("RepoLayerSource")
103            .field("layer_verity", &self.layer_verity)
104            .finish_non_exhaustive()
105    }
106}
107
108impl<ObjectID> LayerSource for RepoLayerSource<ObjectID>
109where
110    ObjectID: FsVerityHashValue,
111{
112    fn open(&self) -> Result<(Vec<OwnedFd>, Box<dyn Send + 'static>)> {
113        let objects_dir = self
114            .repo
115            .objects_dir()
116            .context("opening repository objects dir")?;
117        let dup = rustix::io::dup(objects_dir.as_fd())
118            .map_err(std::io::Error::from)
119            .context("dup objects_dir fd")?;
120        // No external lock needed for the repo source; the guard is a no-op.
121        Ok((vec![dup], Box::new(())))
122    }
123
124    fn produce(&self, dirfd_index_map: &[u32], out: Box<dyn Write + Send>) -> Result<()> {
125        // The objects dir is at chain layer index 0 → dirfd_index_map[0].
126        let objects_dirfd_index = dirfd_index_map
127            .first()
128            .copied()
129            .context("dirfd_index_map is empty: expected at least one real dir")?;
130        produce_layer_splitdirfdstream(&self.repo, &self.layer_verity, objects_dirfd_index, out)
131    }
132}
133
134// ── serve_get_layer ───────────────────────────────────────────────────────────
135
136/// Ready frame batches returned by [`serve_get_layer`].
137///
138/// The caller wraps these in interface-specific zlink `Reply` frames and
139/// yields them from the streaming `GetLayer` method.
140#[derive(Debug)]
141pub struct GetLayerFrames {
142    /// Number of dirfd slots in the sparse layout (`fds[1..=dir_count]`).
143    pub dir_count: u32,
144    /// FD batches in frame order; one inner `Vec<OwnedFd>` per transport frame.
145    pub batches: Vec<Vec<OwnedFd>>,
146}
147
148/// Error from [`serve_get_layer`].
149#[derive(Debug)]
150pub enum ServeGetLayerError {
151    /// `more=false` but total fd count exceeds `MAX_FDS_PER_FRAME`.
152    FdLimitExceeded(FdLimitError),
153    /// Any other I/O or source error.
154    Other(anyhow::Error),
155}
156
157impl From<anyhow::Error> for ServeGetLayerError {
158    fn from(e: anyhow::Error) -> Self {
159        ServeGetLayerError::Other(e)
160    }
161}
162
163/// Drive the full `GetLayer` flow for a [`LayerSource`] and return ready frame
164/// batches.
165///
166/// This is the single canonical implementation of the fd-layout + producer +
167/// framing logic for the repo `GetLayer` path.  The cstor `GetLayer` path in
168/// `composefs-storage` cannot call this function (dep cycle), so it calls
169/// `build_layer_fd_layout` and `spawn_self_reaping_producer` directly.
170/// Both paths share the same primitives from `composefs-splitdirfdstream`.
171///
172/// # Arguments
173/// * `source` — The layer source.  `source.open()` is called once; the returned
174///   guard is moved into the self-reaping producer task.
175/// * `seed` — Deterministic seed for sparse layout and frame count.
176/// * `more` — Whether the client requested multi-frame streaming (`more=true`)
177///   or a single-frame reply (`more=false`).
178///
179/// # Returns
180/// `Ok(GetLayerFrames)` — ready to wrap in zlink replies.
181/// `Err(ServeGetLayerError::FdLimitExceeded)` — total fd count exceeds
182///   `MAX_FDS_PER_FRAME`; retry with `more=true`.
183/// `Err(ServeGetLayerError::Other)` — source or I/O error.
184pub fn serve_get_layer(
185    source: impl LayerSource,
186    seed: u64,
187    more: bool,
188) -> std::result::Result<GetLayerFrames, ServeGetLayerError> {
189    // Open real dirfds + acquire lifetime guard.
190    let (real_fds, guard) = source.open()?;
191
192    // Create the data pipe.
193    let (pipe_read, pipe_write) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)
194        .map_err(|e| anyhow::anyhow!("pipe: {e}"))?;
195
196    // Build the sparse fd layout (dirfds region + keepalive + extra lifetime fds).
197    let layout = build_layer_fd_layout(pipe_read, real_fds, seed)
198        .map_err(|e| anyhow::anyhow!("build_layer_fd_layout: {e}"))?;
199
200    let dir_count = layout.dir_count;
201    let real_indices = layout.real_indices.clone();
202    let keepalive_read = layout.keepalive_read;
203
204    // Split into transport frames (enforces more=false single-frame cap).
205    let batches = split_fds_into_frames(layout.fds_all, seed, more)
206        .map_err(|(_fds, e)| ServeGetLayerError::FdLimitExceeded(e))?;
207
208    // Spawn the 3-phase self-reaping producer.
209    // `source` and `guard` are both moved into the closure.
210    // `guard` drops last (Phase 3), releasing any held lock.
211    spawn_self_reaping_producer(pipe_write, keepalive_read, guard, move |wf| {
212        if let Err(e) = source.produce(&real_indices, Box::new(wf)) {
213            tracing::warn!("GetLayer producer error: {e:#}");
214        }
215    });
216
217    Ok(GetLayerFrames { dir_count, batches })
218}