1use std::path::{Path, PathBuf};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use sha2::{Digest, Sha256};
5use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
6
7#[cfg(unix)]
8use crate::transaction::path_validation::validate_distinct_paths;
9use crate::{Result, UpdateDirective, UpdateError, Updater, reject_executable_leaf_symlink};
10
11static STAGING_COUNTER: AtomicU64 = AtomicU64::new(0);
12#[cfg(unix)]
13pub(crate) const VALIDATION_MODE: u32 = 0o700;
14#[cfg(unix)]
15const UNSAFE_EXECUTABLE_MODE_BITS: u32 = 0o7022;
16#[cfg(unix)]
17const UNSAFE_EXECUTABLE_MODE_REMEDIATION: &str = "grant owner execute and remove special bits and group/other write permissions before staging an update";
18
19#[derive(Debug)]
21pub struct StagedArtifact {
22 pub(crate) path: PathBuf,
23 pub(crate) target_version: String,
24 pub(crate) sha256: String,
25 bytes_written: u64,
26 cleanup_on_drop: bool,
27 #[cfg(unix)]
28 pub(crate) intended_mode: u32,
29 #[cfg(unix)]
30 source_identity: ExecutableIdentity,
31}
32
33#[cfg(unix)]
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35enum ExecutableIdentity {
36 Present { device: u64, inode: u64, mode: u32 },
37 Absent,
38}
39
40impl StagedArtifact {
41 pub fn path(&self) -> &Path {
42 &self.path
43 }
44 pub fn target_version(&self) -> &str {
45 &self.target_version
46 }
47 pub fn sha256(&self) -> &str {
48 &self.sha256
49 }
50 pub fn bytes_written(&self) -> u64 {
51 self.bytes_written
52 }
53
54 #[cfg(unix)]
55 pub(crate) fn source_was_present(&self) -> bool {
56 matches!(self.source_identity, ExecutableIdentity::Present { .. })
57 }
58
59 #[cfg(unix)]
60 pub(crate) fn revalidate_source_executable(&self, path: &Path) -> Result<()> {
61 let (_, current) = executable_mode_and_identity(path)?;
62 if current != self.source_identity {
63 return Err(UpdateError::ExecutableIdentityChanged {
64 path: path.to_path_buf(),
65 });
66 }
67 Ok(())
68 }
69
70 pub fn cleanup(mut self) -> Result<()> {
75 std::fs::remove_file(&self.path).map_err(|error| UpdateError::io(&self.path, error))?;
76 self.cleanup_on_drop = false;
77 Ok(())
78 }
79}
80
81impl Drop for StagedArtifact {
82 fn drop(&mut self) {
83 if self.cleanup_on_drop {
84 let _ = std::fs::remove_file(&self.path);
85 }
86 }
87}
88
89struct PartialArtifact {
90 path: PathBuf,
91 armed: bool,
92}
93
94impl PartialArtifact {
95 fn disarm(&mut self) {
96 self.armed = false;
97 }
98
99 fn report_error(mut self, operation: UpdateError) -> UpdateError {
100 self.armed = false;
101 match std::fs::remove_file(&self.path) {
102 Ok(()) => operation,
103 Err(error) if error.kind() == std::io::ErrorKind::NotFound => operation,
104 Err(cleanup) => UpdateError::ArtifactCleanupFailed {
105 path: self.path.clone(),
106 operation: Box::new(operation),
107 cleanup,
108 },
109 }
110 }
111}
112
113impl Drop for PartialArtifact {
114 fn drop(&mut self) {
115 if self.armed {
116 let _ = std::fs::remove_file(&self.path);
117 }
118 }
119}
120
121async fn create_partial(path: &Path) -> Result<(tokio::fs::File, PartialArtifact)> {
122 let mut open_options = tokio::fs::OpenOptions::new();
123 open_options.create_new(true).write(true);
124 #[cfg(unix)]
125 {
126 open_options.mode(0o600);
127 }
128 let file = open_options
129 .open(path)
130 .await
131 .map_err(|error| UpdateError::io(path, error))?;
132 let cleanup = PartialArtifact {
133 path: path.to_path_buf(),
134 armed: true,
135 };
136 Ok((file, cleanup))
137}
138
139impl Updater {
140 pub fn preflight_stage(&self) -> Result<()> {
146 self.ensure_layout_bound()?;
147 reject_executable_leaf_symlink(self.layout().executable())?;
148 #[cfg(unix)]
149 {
150 let layout = self.validated_layout()?;
151 executable_mode_and_identity(&layout.executable)?;
152 }
153 Ok(())
154 }
155
156 pub async fn stage<R>(
157 &self,
158 mut reader: R,
159 directive: &UpdateDirective,
160 ) -> Result<StagedArtifact>
161 where
162 R: AsyncRead + Unpin,
163 {
164 self.preflight_stage()?;
165 #[cfg(unix)]
166 let layout = self.validated_layout()?;
167 #[cfg(unix)]
168 let resolved_executable = &layout.executable;
169 #[cfg(not(unix))]
170 let resolved_executable = self.layout().executable();
171 let directory = resolved_executable
172 .parent()
173 .filter(|parent| !parent.as_os_str().is_empty())
174 .unwrap_or_else(|| Path::new("."));
175 let name = resolved_executable
176 .file_name()
177 .and_then(|name| name.to_str())
178 .unwrap_or("executable");
179 let path = directory.join(format!(
180 ".{name}.update-{}-{}.part",
181 std::process::id(),
182 STAGING_COUNTER.fetch_add(1, Ordering::Relaxed)
183 ));
184 #[cfg(unix)]
185 let (intended_mode, source_identity) = executable_mode_and_identity(resolved_executable)?;
186 #[cfg(unix)]
187 {
188 let mut namespace = layout.protected.clone();
189 namespace.push(path.clone());
190 validate_distinct_paths(&namespace)?;
191 }
192 let (mut file, mut cleanup) = create_partial(&path).await?;
193 let result: Result<(u64, String)> = async {
194 let mut buffer = [0_u8; 64 * 1024];
195 let mut total = 0_u64;
196 let mut hasher = Sha256::new();
197 loop {
198 let read = reader
199 .read(&mut buffer)
200 .await
201 .map_err(|error| UpdateError::io(&path, error))?;
202 if read == 0 {
203 break;
204 }
205 let next = total.saturating_add(read as u64);
206 if next > self.policy().max_artifact_bytes() {
207 return Err(UpdateError::ArtifactTooLarge {
208 limit: self.policy().max_artifact_bytes(),
209 actual: next,
210 });
211 }
212 file.write_all(&buffer[..read])
213 .await
214 .map_err(|error| UpdateError::io(&path, error))?;
215 hasher.update(&buffer[..read]);
216 total = next;
217 }
218 file.flush()
219 .await
220 .map_err(|error| UpdateError::io(&path, error))?;
221 file.sync_all()
222 .await
223 .map_err(|error| UpdateError::io(&path, error))?;
224 let actual = encode_hex(&hasher.finalize());
225 if actual != directive.sha256() {
226 return Err(UpdateError::DigestMismatch {
227 expected: directive.sha256().to_owned(),
228 actual,
229 });
230 }
231 #[cfg(unix)]
232 {
233 use std::os::unix::fs::PermissionsExt;
234 file.set_permissions(std::fs::Permissions::from_mode(VALIDATION_MODE))
235 .await
236 .map_err(|error| UpdateError::io(&path, error))?;
237 file.sync_all()
238 .await
239 .map_err(|error| UpdateError::io(&path, error))?;
240 }
241 Ok((total, actual))
242 }
243 .await;
244 drop(file.into_std().await);
247 let (total, actual) = match result {
248 Ok(result) => result,
249 Err(operation) => return Err(cleanup.report_error(operation)),
250 };
251 cleanup.disarm();
252 Ok(StagedArtifact {
253 path,
254 target_version: directive.version().to_owned(),
255 sha256: actual,
256 bytes_written: total,
257 cleanup_on_drop: true,
258 #[cfg(unix)]
259 intended_mode,
260 #[cfg(unix)]
261 source_identity,
262 })
263 }
264}
265
266#[cfg(unix)]
267fn executable_mode_and_identity(path: &Path) -> Result<(u32, ExecutableIdentity)> {
268 use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
269
270 let file = match std::fs::OpenOptions::new()
271 .read(true)
272 .custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_NONBLOCK)
273 .open(path)
274 {
275 Ok(file) => file,
276 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
277 return Ok((0o700, ExecutableIdentity::Absent));
278 }
279 Err(error) => return Err(UpdateError::io(path, error)),
280 };
281 let metadata = file
282 .metadata()
283 .map_err(|error| UpdateError::io(path, error))?;
284 if !metadata.file_type().is_file() {
285 return Err(UpdateError::InvalidPolicy(
286 "executable path must be a regular file",
287 ));
288 }
289 let mode = metadata.permissions().mode() & 0o7777;
290 validate_executable_mode(path, mode)?;
291 Ok((
292 mode,
293 ExecutableIdentity::Present {
294 device: metadata.dev(),
295 inode: metadata.ino(),
296 mode,
297 },
298 ))
299}
300
301#[cfg(unix)]
302pub(crate) fn validate_executable_mode(path: &Path, mode: u32) -> Result<()> {
303 let mode = mode & 0o7777;
304 if mode & UNSAFE_EXECUTABLE_MODE_BITS != 0 || mode & 0o100 == 0 {
305 return Err(UpdateError::UnsafeExecutableMode {
306 path: path.to_path_buf(),
307 mode,
308 remediation: UNSAFE_EXECUTABLE_MODE_REMEDIATION,
309 });
310 }
311 Ok(())
312}
313
314fn encode_hex(bytes: &[u8]) -> String {
315 const HEX: &[u8; 16] = b"0123456789abcdef";
316 let mut output = String::with_capacity(bytes.len() * 2);
317 for byte in bytes {
318 output.push(HEX[(byte >> 4) as usize] as char);
319 output.push(HEX[(byte & 0x0f) as usize] as char);
320 }
321 output
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[tokio::test]
329 async fn create_partial_collision_preserves_preexisting_sentinel() {
330 let temp = tempfile::tempdir().unwrap();
331 let explicit_path = temp.path().join("explicit-collision.part");
332 let sentinel = b"not owned by the updater";
333 std::fs::write(&explicit_path, sentinel).unwrap();
334
335 assert!(create_partial(&explicit_path).await.is_err());
336 assert_eq!(std::fs::read(&explicit_path).unwrap(), sentinel);
337 }
338}