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