eggup_core/current_exe.rs
1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::error::{Error, Result};
5
6/// The exact executable that is currently running, bound once to a canonical target.
7///
8/// Binding resolves symlinks, so an invocation through a link updates the real
9/// image and never overwrites the link object. It also proves that the target is
10/// a regular file beneath a real directory and records an identity for it: on
11/// Unix that is device plus inode, and on **every** platform it is the SHA-256
12/// of the image's bytes.
13///
14/// The content digest is what makes the binding safe on Windows. A file
15/// identity there needs handle-based FFI, which this crate forbids, so a
16/// path-only recheck would silently accept an image swapped in between binding
17/// and commit. Content equality is the strongest identity provable with safe
18/// code on every platform, and a mismatch fails closed.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CurrentExecutable {
21 target: PathBuf,
22 installation_root: PathBuf,
23 destination_name: PathBuf,
24 digest: [u8; 32],
25 #[cfg(unix)]
26 device: u64,
27 #[cfg(unix)]
28 inode: u64,
29}
30
31impl CurrentExecutable {
32 /// Resolves and binds the running executable from the process environment.
33 ///
34 /// `std::env::current_exe` is only a starting point: on some platforms it
35 /// reports the invocation path, which may traverse symlinks. The result is
36 /// always canonicalized and re-proven before it is used for mutation.
37 pub fn resolve() -> Result<Self> {
38 let reported = std::env::current_exe()
39 .map_err(|source| Error::io("resolving current executable", source))?;
40 Self::bind(reported)
41 }
42
43 /// Binds an already-known executable path, applying the same proof rules as
44 /// [`resolve`](Self::resolve).
45 ///
46 /// This is the seam a child-process fixture uses to address a specific
47 /// image; production callers normally use [`resolve`](Self::resolve).
48 pub fn bind(executable: impl Into<PathBuf>) -> Result<Self> {
49 let reported = executable.into();
50 if !reported.is_absolute() {
51 return Err(Error::invalid("current executable path must be absolute"));
52 }
53 // Canonicalization is what makes an invocation symlink safe: the link
54 // object is never the destination, only its real target is.
55 let target = fs::canonicalize(&reported)
56 .map_err(|source| Error::io("canonicalizing current executable", source))?;
57 if target.as_os_str().is_empty() {
58 return Err(Error::invalid(
59 "current executable resolved to an empty path",
60 ));
61 }
62 let metadata = fs::symlink_metadata(&target)
63 .map_err(|source| Error::io("reading current executable metadata", source))?;
64 if metadata.file_type().is_symlink() || !metadata.is_file() {
65 return Err(Error::DestinationConflict {
66 destination: target,
67 });
68 }
69 #[cfg(unix)]
70 {
71 use std::os::unix::fs::MetadataExt;
72 // A hard-linked image has other names whose contents would silently
73 // change with it, so exact identity is not provable and the update
74 // is refused.
75 if metadata.nlink() != 1 {
76 return Err(Error::DestinationConflict {
77 destination: target,
78 });
79 }
80 }
81 let installation_root = target
82 .parent()
83 .ok_or_else(|| Error::invalid("current executable has no parent directory"))?
84 .to_path_buf();
85 let root_metadata = fs::symlink_metadata(&installation_root)
86 .map_err(|source| Error::io("reading executable directory metadata", source))?;
87 if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
88 return Err(Error::DestinationConflict {
89 destination: target,
90 });
91 }
92 let destination_name = PathBuf::from(
93 target
94 .file_name()
95 .ok_or_else(|| Error::invalid("current executable has no file name"))?,
96 );
97 #[cfg(unix)]
98 let (device, inode) = {
99 use std::os::unix::fs::MetadataExt;
100 (metadata.dev(), metadata.ino())
101 };
102 // Read once here so a later swap is detectable without any unsafe code.
103 let digest = crate::integrity::hash_file(&target)?;
104 Ok(Self {
105 target,
106 installation_root,
107 destination_name,
108 digest,
109 #[cfg(unix)]
110 device,
111 #[cfg(unix)]
112 inode,
113 })
114 }
115
116 /// Returns the canonical live path that will be replaced.
117 pub fn target(&self) -> &Path {
118 &self.target
119 }
120
121 /// Returns the executable's own directory, which is the installation root
122 /// used by [`crate::InstallPlan::for_current_executable`].
123 ///
124 /// The whole self-update transaction stays inside this directory, so no
125 /// write authority above it is ever required.
126 pub fn installation_root(&self) -> &Path {
127 &self.installation_root
128 }
129
130 /// Returns the file name used as the single destination member, relative to
131 /// [`installation_root`](Self::installation_root).
132 pub fn destination_name(&self) -> &Path {
133 &self.destination_name
134 }
135
136 /// Returns the absolute destination path of the running image.
137 pub fn destination(&self) -> PathBuf {
138 self.installation_root.join(&self.destination_name)
139 }
140
141 /// Re-proves that this exact object is still the bound current executable.
142 ///
143 /// Called immediately before live mutation under the mutation lock. A
144 /// replaced, swapped, or re-linked image fails closed instead of being
145 /// overwritten.
146 pub(crate) fn revalidate(&self) -> Result<()> {
147 let metadata = match fs::symlink_metadata(&self.target) {
148 Ok(metadata) => metadata,
149 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
150 return Err(Error::DestinationConflict {
151 destination: self.target.clone(),
152 })
153 }
154 Err(source) => {
155 return Err(Error::io(
156 "reading current executable before mutation",
157 source,
158 ))
159 }
160 };
161 if metadata.file_type().is_symlink() || !metadata.is_file() {
162 return Err(Error::DestinationConflict {
163 destination: self.target.clone(),
164 });
165 }
166 #[cfg(unix)]
167 {
168 use std::os::unix::fs::MetadataExt;
169 if metadata.nlink() != 1
170 || metadata.dev() != self.device
171 || metadata.ino() != self.inode
172 {
173 return Err(Error::DestinationConflict {
174 destination: self.target.clone(),
175 });
176 }
177 }
178 // Checked on every platform. Without this, the Windows build has *no*
179 // identity proof at all — device and inode do not exist there — and
180 // would commit over an image swapped in after binding.
181 match crate::integrity::hash_file(&self.target) {
182 Ok(digest) if digest == self.digest => Ok(()),
183 Ok(_) => Err(Error::DestinationConflict {
184 destination: self.target.clone(),
185 }),
186 // Already a typed crate error; propagate it unchanged so the
187 // diagnostic names the real filesystem failure.
188 Err(error) => Err(error),
189 }
190 }
191}