1use std::fs::{File, TryLockError};
22use std::io;
23use std::path::{Path, PathBuf};
24
25use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
26#[cfg(unix)]
27use cap_std::fs::PermissionsExt;
28use cap_std::fs::{Dir, OpenOptions};
29
30use crate::filesystem::{ConfinedDir, PRIVATE_DIR_MODE, PRIVATE_FILE_MODE, note_open};
31
32const WRITER_LOCK_FILE: &str = "writer.lock";
34
35#[derive(Debug, thiserror::Error)]
37pub enum HardenError {
38 #[error(
40 "the store at `{data_dir}` is held by a live server: its writer lock at `{lock_path}` \
41 is taken. Stop the server (`aion stop`), run this verb, then start it again — the \
42 walk needs the store to itself"
43 )]
44 WriterLockHeld {
45 data_dir: PathBuf,
47 lock_path: PathBuf,
49 },
50 #[error("could not take the store's writer lock at `{lock_path}`: {error}")]
52 Lock {
53 lock_path: PathBuf,
55 #[source]
57 error: io::Error,
58 },
59 #[error("could not open the store at `{data_dir}`: {error}")]
61 Open {
62 data_dir: PathBuf,
64 #[source]
66 error: io::Error,
67 },
68 #[error("hardening `{entry}` under `{data_dir}` failed: {error}")]
70 Walk {
71 data_dir: PathBuf,
73 entry: String,
75 #[source]
77 error: io::Error,
78 },
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub struct HardenReport {
84 pub directories: u64,
86 pub files: u64,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct HardenProgress {
94 pub entry: String,
96 pub files: u64,
98 pub total_files: u64,
100}
101
102pub fn harden_store_tree(
112 data_dir: &Path,
113 mut progress: impl FnMut(&HardenProgress),
114) -> Result<HardenReport, HardenError> {
115 let _held = take_writer_lock(data_dir)?;
116 let root = ConfinedDir::open(data_dir).map_err(|error| HardenError::Open {
117 data_dir: data_dir.to_path_buf(),
118 error,
119 })?;
120 let mut report = HardenReport {
121 directories: 1,
122 files: 0,
123 };
124 let mut walk = |report: &mut HardenReport| -> io::Result<()> {
125 let mut entries = Vec::new();
126 for entry in root.dir().entries()? {
127 let entry = entry?;
128 entries.push((entry.file_name(), entry.file_type()?));
129 }
130 entries.sort_by(|left, right| left.0.cmp(&right.0));
131 for (name, file_type) in entries {
132 let entry = name.to_string_lossy().into_owned();
133 let files_before = report.files;
134 harden_entry(root.dir(), &name, file_type, report)
135 .map_err(|error| io::Error::new(error.kind(), format!("{entry}: {error}")))?;
136 progress(&HardenProgress {
137 entry,
138 files: report.files - files_before,
139 total_files: report.files,
140 });
141 }
142 Ok(())
143 };
144 walk(&mut report).map_err(|error| {
145 let (entry, error) = split_entry(error);
146 HardenError::Walk {
147 data_dir: data_dir.to_path_buf(),
148 entry,
149 error,
150 }
151 })?;
152 Ok(report)
153}
154
155fn split_entry(error: io::Error) -> (String, io::Error) {
159 let text = error.to_string();
160 match text.split_once(": ") {
161 Some((entry, rest)) => (
162 entry.to_owned(),
163 io::Error::new(error.kind(), rest.to_owned()),
164 ),
165 None => (String::new(), error),
166 }
167}
168
169fn take_writer_lock(data_dir: &Path) -> Result<Option<File>, HardenError> {
172 let lock_path = data_dir.join(WRITER_LOCK_FILE);
173 let file = match File::options().read(true).write(true).open(&lock_path) {
174 Ok(file) => file,
175 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
176 Err(error) => return Err(HardenError::Lock { lock_path, error }),
177 };
178 match file.try_lock() {
179 Ok(()) => Ok(Some(file)),
180 Err(TryLockError::WouldBlock) => Err(HardenError::WriterLockHeld {
181 data_dir: data_dir.to_path_buf(),
182 lock_path,
183 }),
184 Err(TryLockError::Error(error)) => Err(HardenError::Lock { lock_path, error }),
185 }
186}
187
188fn harden_entry(
189 parent: &Dir,
190 name: &std::ffi::OsStr,
191 file_type: cap_std::fs::FileType,
192 report: &mut HardenReport,
193) -> io::Result<()> {
194 if file_type.is_symlink() {
195 return Err(io::Error::new(
196 io::ErrorKind::InvalidInput,
197 "sensitive state contains a symbolic link",
198 ));
199 }
200 if file_type.is_dir() {
201 note_open();
202 let child = parent.open_dir_nofollow(name)?;
203 harden_dir(&child, report)
204 } else if file_type.is_file() {
205 harden_file(parent, name, report)
206 } else {
207 Err(io::Error::new(
208 io::ErrorKind::InvalidInput,
209 "sensitive state contains something that is neither a file nor a directory",
210 ))
211 }
212}
213
214fn harden_dir(dir: &Dir, report: &mut HardenReport) -> io::Result<()> {
215 #[cfg(unix)]
216 dir.set_permissions(
217 Path::new("."),
218 cap_std::fs::Permissions::from_mode(PRIVATE_DIR_MODE),
219 )?;
220 report.directories += 1;
221 for entry in dir.entries()? {
222 let entry = entry?;
223 let name = entry.file_name();
224 let file_type = entry.file_type()?;
225 harden_entry(dir, &name, file_type, report).map_err(|error| {
226 io::Error::new(error.kind(), format!("{}: {error}", name.to_string_lossy()))
227 })?;
228 }
229 Ok(())
230}
231
232fn harden_file(parent: &Dir, name: &std::ffi::OsStr, report: &mut HardenReport) -> io::Result<()> {
233 let mut options = OpenOptions::new();
234 options.read(true).follow(FollowSymlinks::No);
235 note_open();
236 let file = parent.open_with(name, &options)?;
237 #[cfg(unix)]
238 file.set_permissions(cap_std::fs::Permissions::from_mode(PRIVATE_FILE_MODE))?;
239 #[cfg(not(unix))]
240 drop(file);
241 report.files += 1;
242 Ok(())
243}
244
245#[cfg(test)]
246#[path = "store_harden_tests.rs"]
247mod tests;