Skip to main content

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}