damascus/os/linux/
overlay.rs

1// Copyright 2025 Yato202010
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8/*
9* Implementation inspired by libmount crate
10* https://github.com/tailhook/libmount/blob/master/src/overlay.rs
11*
12*/
13
14mod opt;
15use crate::common::fs::MountOption;
16pub use opt::*;
17
18use nix::mount::{MntFlags, MsFlags, mount, umount2};
19use std::{
20    ffi::CString,
21    io::{Error, ErrorKind, Result},
22    ops::Deref,
23    path::{Path, PathBuf},
24};
25use tracing::{debug, error};
26
27use crate::{
28    AsCString, AsPath, Filesystem, FsData, PartitionID, StackableFilesystem, StateRecovery,
29    restore_fsdata,
30};
31
32#[derive(Debug)]
33/// Kernel overlay filesystem handle
34pub struct OverlayFs {
35    lower: Vec<PathBuf>,
36    upper: Option<PathBuf>,
37    work: Option<PathBuf>,
38    target: CString,
39    options: Vec<String>,
40    id: Option<PartitionID>,
41    drop: bool,
42}
43
44impl OverlayFs {
45    #[must_use = "initialised OverlayFs handle should be used"]
46    #[inline]
47    pub fn new(
48        lower: impl IntoIterator<Item = impl AsRef<Path>>,
49        upper: Option<impl Into<PathBuf>>,
50        work: Option<impl Into<PathBuf>>,
51        target: impl AsRef<Path>,
52        drop: bool,
53    ) -> Result<OverlayFs> {
54        Ok(Self {
55            lower: lower
56                .into_iter()
57                .map(|x| x.as_ref().to_path_buf())
58                .collect(),
59            upper: upper.map(|x| x.into()),
60            work: work.map(|x| x.into()),
61            target: target.as_ref().as_cstring(),
62            options: OverlayFsOption::defaults(),
63            id: None,
64            drop,
65        })
66    }
67
68    #[must_use = "initialised OverlayFs handle should be used"]
69    #[inline]
70    pub fn readonly(
71        lower: impl IntoIterator<Item = impl AsRef<Path>>,
72        target: impl AsRef<Path>,
73    ) -> Result<OverlayFs> {
74        let lower: Vec<PathBuf> = lower
75            .into_iter()
76            .map(|x| x.as_ref().to_path_buf())
77            .collect();
78        if lower.len() < 2 {
79            return Err(Error::other(
80                "overlay FileSystem need a least 2 lower directory to work",
81            ));
82        }
83        Ok(OverlayFs {
84            lower,
85            upper: None,
86            work: None,
87            target: target.as_ref().as_cstring(),
88            options: OverlayFsOption::defaults(),
89            id: None,
90            drop: true,
91        })
92    }
93
94    #[must_use = "initialised OverlayFs handle should be used"]
95    #[inline]
96    pub fn writable(
97        lower: impl IntoIterator<Item = impl AsRef<Path>>,
98        upper: impl AsRef<Path>,
99        work: impl AsRef<Path>,
100        target: impl AsRef<Path>,
101    ) -> Result<OverlayFs> {
102        if PartitionID::try_from(upper.as_ref())? != PartitionID::try_from(work.as_ref())? {
103            return Err(Error::other(
104                "overlay FileSystem need the upper dir and the work dir to be on the same FileSystem",
105            ));
106        }
107        Ok(OverlayFs {
108            lower: lower
109                .into_iter()
110                .map(|x| x.as_ref().to_path_buf())
111                .collect(),
112            upper: Some(upper.as_ref().to_path_buf()),
113            work: Some(work.as_ref().to_path_buf()),
114            target: target.as_ref().as_cstring(),
115            options: OverlayFsOption::defaults(),
116            id: None,
117            drop: true,
118        })
119    }
120
121    #[inline]
122    pub fn work(&self) -> Option<&PathBuf> {
123        self.work.as_ref()
124    }
125
126    #[inline]
127    pub fn set_work(&mut self, work: PathBuf) -> Result<()> {
128        if PartitionID::try_from(work.as_path())?
129            != PartitionID::try_from(
130                self.upper
131                    .as_ref()
132                    .ok_or(Error::new(ErrorKind::NotFound, "upper directory not set"))?
133                    .as_path(),
134            )?
135        {
136            return Err(Error::other(
137                "overlay FileSystem need the upper dir and the work dir to be on the same FileSystem",
138            ));
139        }
140        self.work = Some(work);
141        Ok(())
142    }
143}
144
145impl Filesystem for OverlayFs {
146    #[inline]
147    fn mount(&mut self) -> Result<&mut Self> {
148        if !Self::is_available() {
149            return Err(Error::new(
150                ErrorKind::NotFound,
151                "overlayfs is not available",
152            ));
153        }
154        if self.mounted() {
155            debug!("Damascus: partition already mounted");
156            return Ok(self);
157        }
158        let flags = MsFlags::empty();
159        let mut options = String::new();
160        options.push_str("lowerdir=");
161        for (i, p) in self.lower.iter().enumerate() {
162            if i != 0 {
163                options.push(':')
164            }
165            options.push_str(p.to_string_lossy().as_ref());
166        }
167        if let (Some(u), Some(w)) = (self.upper.as_ref(), self.work.as_ref()) {
168            options.push_str(",upperdir=");
169            options.push_str(u.to_string_lossy().as_ref());
170            options.push_str(",workdir=");
171            options.push_str(w.to_string_lossy().as_ref());
172        }
173        for mo in &self.options {
174            options.push_str(&(",".to_string() + &mo.to_string()))
175        }
176        let mut args = options.as_bytes().to_vec();
177        args.push(b'\0');
178        let data = unsafe { CString::from_vec_with_nul_unchecked(args) };
179        mount(
180            Some(c"overlay"),
181            &*self.target,
182            Some(c"overlay"),
183            flags,
184            Some(data.as_bytes()),
185        )
186        .inspect_err(|_x| {
187            dbg!(&self);
188        })?;
189        self.id = Some(
190            PartitionID::try_from(self.target.as_path())
191                .map_err(|_| Error::other("unable to get PartitionID"))?,
192        );
193        Ok(self)
194    }
195
196    #[inline]
197    fn unmount(&mut self) -> Result<&mut Self> {
198        if self.mounted() {
199            umount2(self.target.as_c_str(), MntFlags::MNT_DETACH)?;
200            self.id = None;
201        }
202        Ok(self)
203    }
204
205    #[inline]
206    fn scoped(&self) -> bool {
207        self.drop
208    }
209
210    #[inline]
211    fn set_scoped(&mut self, drop: bool) -> &mut Self {
212        self.drop = drop;
213        self
214    }
215
216    #[inline]
217    fn id(&self) -> Option<&PartitionID> {
218        self.id.as_ref()
219    }
220
221    #[inline]
222    fn target(&self) -> PathBuf {
223        self.target.as_path().to_path_buf()
224    }
225
226    #[inline]
227    fn set_target(&mut self, target: impl AsRef<Path>) -> Result<&mut Self> {
228        if self.id.is_some() {
229            return Err(Error::other(
230                "mount point cannot be change when the FileSystem is mounted",
231            ));
232        }
233        self.target = target.as_ref().as_cstring();
234        Ok(self)
235    }
236
237    fn is_available() -> bool {
238        if let Ok(res) = std::fs::read_to_string("/proc/filesystems") {
239            res.contains("overlay")
240        } else {
241            false
242        }
243    }
244
245    fn add_option(&mut self, option: impl Into<String>) -> Result<()> {
246        self.options.push(option.into());
247        Ok(())
248    }
249
250    fn remove_option(&mut self, option: impl AsRef<str>) -> Result<()> {
251        self.options.retain(|x| x.deref() != option.as_ref());
252        Ok(())
253    }
254
255    fn options(&self) -> &[String] {
256        self.options.deref()
257    }
258}
259
260impl StackableFilesystem for OverlayFs {
261    #[inline]
262    fn lower(&self) -> Vec<&Path> {
263        self.lower.iter().map(|x| x.as_path()).collect()
264    }
265
266    #[inline]
267    fn set_lower(
268        &mut self,
269        lower: impl IntoIterator<Item = impl AsRef<Path>>,
270    ) -> Result<&mut Self> {
271        if self.id.is_some() {
272            return Err(Error::other(
273                "upper layer cannot be change when the FileSystem is mounted",
274            ));
275        }
276        self.lower = lower
277            .into_iter()
278            .map(|x| x.as_ref().to_path_buf())
279            .collect();
280        Ok(self)
281    }
282
283    #[inline]
284    fn upper(&self) -> Option<&Path> {
285        self.upper.as_deref()
286    }
287
288    #[inline]
289    fn set_upper(&mut self, upper: impl Into<PathBuf>) -> Result<&mut Self> {
290        let upper = upper.into();
291        if PartitionID::try_from(upper.as_path())?
292            != PartitionID::try_from(
293                self.work
294                    .as_ref()
295                    .ok_or(Error::new(ErrorKind::NotFound, "work directory not set"))?
296                    .as_path(),
297            )?
298        {
299            return Err(Error::other(
300                "overlay FileSystem need the upper dir and the work dir to be on the same FileSystem",
301            ));
302        } else if self.id.is_some() {
303            return Err(Error::other(
304                "upper layer cannot be change when the FileSystem is mounted",
305            ));
306        }
307        self.upper = Some(upper);
308        Ok(self)
309    }
310}
311
312impl StateRecovery for OverlayFs {
313    fn recover<P: AsRef<Path>>(path: P) -> Result<Self> {
314        let path = path.as_ref();
315        let data: FsData = restore_fsdata(path)?.ok_or(Error::new(
316            ErrorKind::NotFound,
317            "OverlayFs not found at mount point : ".to_string() + &path.to_string_lossy(),
318        ))?;
319        let mut lower = vec![];
320        let mut upper = None;
321        let mut work = None;
322        let target = path.as_cstring();
323        let options = data
324            .options()
325            .iter()
326            .filter_map(|x| {
327                let (o, va) = if let Some(x) = x.split_once('=') {
328                    x
329                } else {
330                    return Some(x.to_owned());
331                };
332                match o {
333                    "lowerdir" => {
334                        for path in va.split(':') {
335                            lower.push(PathBuf::from(path))
336                        }
337                        return None;
338                    }
339                    "upperdir" => {
340                        upper = Some(PathBuf::from(va));
341                        return None;
342                    }
343                    "workdir" => {
344                        work = Some(PathBuf::from(va));
345                        return None;
346                    }
347                    _ => {}
348                }
349                Some(x.to_owned())
350            })
351            .collect();
352        Ok(Self {
353            lower,
354            upper,
355            work,
356            target,
357            options,
358            id: Some(
359                PartitionID::try_from(path)
360                    .map_err(|_| Error::other("unable to get PartitionID"))?,
361            ),
362            drop: false,
363        })
364    }
365}
366
367impl Drop for OverlayFs {
368    #[inline]
369    fn drop(&mut self) {
370        if self.drop
371            && let Err(err) = self.unmount()
372        {
373            error!(
374                "Damascus: unable to unmount overlay at {:?} because : {}",
375                self.target, err
376            )
377        }
378    }
379}