Skip to main content

xet_runtime/file_utils/
privilege_context.rs

1use std::fs::File;
2#[cfg(unix)]
3use std::os::unix::fs::MetadataExt;
4use std::path::Path;
5
6#[cfg(unix)]
7use colored::Colorize;
8use lazy_static::lazy_static;
9use tracing::error;
10#[cfg(windows)]
11use winapi::um::{
12    processthreadsapi::GetCurrentProcess,
13    processthreadsapi::OpenProcessToken,
14    securitybaseapi::GetTokenInformation,
15    winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation},
16};
17
18#[cfg(test)]
19static mut WARNING_PRINTED: bool = false;
20
21/// Checks if the program is running under elevated privilege
22fn is_elevated_impl() -> bool {
23    // In a Unix-like environment, when a program is run with sudo,
24    // the effective user ID (euid) of the process is set to 0.
25    #[cfg(unix)]
26    {
27        unsafe { libc::geteuid() == 0 }
28    }
29
30    #[cfg(windows)]
31    {
32        let mut token: HANDLE = std::ptr::null_mut();
33        if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 {
34            return false;
35        }
36
37        let mut elevation: TOKEN_ELEVATION = unsafe { std::mem::zeroed() };
38        let mut return_length = 0;
39        let success = unsafe {
40            GetTokenInformation(
41                token,
42                TokenElevation,
43                &mut elevation as *mut _ as *mut _,
44                std::mem::size_of::<TOKEN_ELEVATION>() as u32,
45                &mut return_length,
46            )
47        };
48
49        if success == 0 {
50            false
51        } else {
52            elevation.TokenIsElevated != 0
53        }
54    }
55
56    #[cfg(not(any(unix, windows)))]
57    {
58        // For other platforms, we assume not elevated
59        false
60    }
61}
62
63lazy_static! {
64    static ref IS_ELEVATED: bool = is_elevated_impl();
65}
66
67pub fn is_elevated() -> bool {
68    *IS_ELEVATED
69}
70
71// Facts:
72// Assume there's a standard user A that is not a root user.
73// 1. On Unix systems, suppose there is a path 'dir/f' where 'dir' is created by A but 'f' created by 'sudo A', A can
74//    read, rename or remove 'dir/f'. This implies that it's enough to check the permission of 'dir' if we don't
75//    directly write into 'dir/f'. This is exactly how we interact with the xorb cache: if an eviction is deemed
76//    necessary, the replacement data is written to a tempfile first and then renamed to the to-be-evicted entry. So
77//    even if a certain cache file was created by 'sudo A', the eviction by 'A' will succeed.
78// 2. On Windows, 'Run as administrator' by logged in user A actually sets %HOMEPATH% to administrator's HOME, so by
79//    default the xet metadata folders are isolated. If 'run as admin A' explicility configures cache or repo path to
80//    another location owned by A, ACLs for the created path inherit from the parent folder, so A still has full
81//    control.
82
83#[derive(Debug, Clone, Copy)]
84pub enum PrivilegedExecutionContext {
85    Regular,
86    Elevated,
87}
88
89impl PrivilegedExecutionContext {
90    pub fn current() -> PrivilegedExecutionContext {
91        match is_elevated() {
92            false => PrivilegedExecutionContext::Regular,
93            true => PrivilegedExecutionContext::Elevated,
94        }
95    }
96
97    pub fn is_elevated(&self) -> bool {
98        match self {
99            PrivilegedExecutionContext::Regular => false,
100            PrivilegedExecutionContext::Elevated => true,
101        }
102    }
103
104    /// Recursively create a directory and all of its parent components if they are missing for write.
105    /// If the current process is running with elevated privileges, the entries created
106    /// will inherit permission from the path parent.
107    pub fn create_dir_all(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
108        // if path is absolute, cwd is ignored.
109        let path = std::env::current_dir()?.join(path);
110        let path = path.as_path();
111
112        // first find an ancestor of the path that exists.
113        let mut root = path;
114        while !root.exists() {
115            let Some(pparent) = root.parent() else {
116                return Err(std::io::Error::new(
117                    std::io::ErrorKind::InvalidInput,
118                    format!("Path {root:?} has no parent."),
119                ));
120            };
121
122            root = pparent;
123        }
124
125        // try recursively create all the directories.
126        std::fs::create_dir_all(path).inspect_err(|err| {
127            if err.kind() == std::io::ErrorKind::PermissionDenied {
128                permission_warning(root, true);
129            }
130        })?;
131
132        // with elevated privileges we chown for all entries from path to root.
133        // Permission inheriting from the parent is the default behavior on Windows, thus
134        // the below implementation only targets Unix systems.
135        #[cfg(unix)]
136        if self.is_elevated() {
137            let root_meta = std::fs::metadata(root)?;
138            let mut path = path;
139            while path != root {
140                std::os::unix::fs::chown(path, Some(root_meta.uid()), Some(root_meta.gid()))?;
141                let Some(pparent) = path.parent() else {
142                    return Err(std::io::Error::new(
143                        std::io::ErrorKind::InvalidInput,
144                        format!("Path {path:?} has no parent."),
145                    ));
146                };
147                path = pparent;
148            }
149        }
150
151        Ok(())
152    }
153
154    /// Open or create a file for write.
155    /// If the current process is running with elevated privileges, the entries created
156    /// will inherit permission from the path parent.
157    pub fn create_file(&self, path: impl AsRef<Path>) -> std::io::Result<File> {
158        // if path is absolute, cwd is ignored.
159        let path = std::env::current_dir()?.join(path);
160        let path = path.as_path();
161
162        let Some(pparent) = path.parent() else {
163            return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Path {path:?} has no parent.")));
164        };
165
166        self.create_dir_all(pparent)?;
167
168        #[allow(unused_variables)]
169        let parent_meta = std::fs::metadata(pparent)?;
170
171        #[cfg(unix)]
172        let exist = path.exists();
173
174        let create = || {
175            std::fs::OpenOptions::new()
176                .create(true)
177                .truncate(false)
178                .write(true)
179                .open(path)
180                .inspect_err(|err| {
181                    if err.kind() == std::io::ErrorKind::PermissionDenied {
182                        permission_warning(path, false);
183                    }
184                })
185        };
186
187        // Test if the current context has write access to the file.
188        create()?;
189
190        // exist is only trustable if opening file for R+W succeeded.
191        // Permission inheriting from the parent is the default behavior on Windows, thus
192        // the below implementation only targets Unix systems.
193        #[cfg(unix)]
194        if !exist && self.is_elevated() {
195            // changes the ownership.
196            std::os::unix::fs::chown(path, Some(parent_meta.uid()), Some(parent_meta.gid()))?;
197        }
198
199        // Now reopen it.
200        create()
201    }
202}
203
204pub fn create_dir_all(path: impl AsRef<Path>) -> std::io::Result<()> {
205    PrivilegedExecutionContext::current().create_dir_all(path)
206}
207
208pub fn create_file(path: impl AsRef<Path>) -> std::io::Result<File> {
209    PrivilegedExecutionContext::current().create_file(path)
210}
211
212#[allow(unused_variables)]
213fn permission_warning(path: &Path, recursive: bool) {
214    #[cfg(unix)]
215    {
216        let username = whoami::username().unwrap_or_else(|_| "unknown".to_string());
217        let message = format!(
218            "The process doesn't have correct read-write permission into path {path:?}, please resets 
219        ownership by 'sudo chown{}{} {path:?}'.",
220            if recursive { " -R " } else { " " },
221            username
222        );
223
224        eprintln!("{}", message.bright_blue());
225    }
226
227    #[cfg(windows)]
228    eprintln!(
229        "The process doesn't have correct read-write permission into path {path:?}, please resets
230    permission in the Properties dialog box under the Security tab."
231    );
232
233    error!("Permission denied for path {path:?}");
234
235    #[cfg(test)]
236    unsafe {
237        WARNING_PRINTED = true
238    };
239}
240
241#[cfg(all(test, unix))]
242mod test {
243    use std::os::unix::fs::MetadataExt;
244    use std::path::Path;
245
246    use anyhow::Result;
247
248    use super::{PrivilegedExecutionContext, WARNING_PRINTED};
249
250    #[test]
251    #[ignore = "run manually"]
252    fn test_create_dir_all() -> Result<()> {
253        // Run this test manually, steps:
254
255        /* For Unix
256            1. Run the below shell script in an empty dir with standard privileges.
257            2. Set env var 'HF_XET_TEST_PATH' to this path.
258            3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'.
259            4. Locate the path to the executable as TEST_EXE
260            5. Run test with a non-root user: 'TEST_EXE config::permission::test::test_create_dir_all --exact --nocapture --include-ignored'
261            sudo mkdir rootdir
262        */
263
264        let test_path = std::env::var("HF_XET_TEST_PATH")?;
265        std::env::set_current_dir(test_path)?;
266        let permission = PrivilegedExecutionContext::current();
267
268        let test = Path::new("rootdir/regdir1/regdir2");
269
270        assert!(permission.create_dir_all(test).is_err());
271        unsafe { assert!(WARNING_PRINTED) };
272
273        Ok(())
274    }
275
276    #[test]
277    #[ignore = "run manually"]
278    fn test_create_dir_all_sudo() -> Result<()> {
279        // Run this test manually, steps:
280
281        /* For Unix
282            1. Run the below shell script in an empty dir with standard privileges.
283            2. Set env var 'HF_XET_TEST_PATH' to this path.
284            3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'.
285            4. Locate the path to the executable as TEST_EXE
286            5. Run test with a non-root user: 'sudo -E TEST_EXE config::permission::test::test_create_dir_all_sudo --exact --nocapture --include-ignored'
287
288            mkdir regdir
289        */
290
291        let test_path = std::env::var("HF_XET_TEST_PATH")?;
292        std::env::set_current_dir(test_path)?;
293        let permission = PrivilegedExecutionContext::current();
294
295        let test = Path::new("regdir/regdir1/regdir2");
296
297        permission.create_dir_all(test)?;
298
299        assert!(test.exists());
300
301        // not owned by root
302        assert!(std::fs::metadata(test)?.uid() != 0);
303
304        let parent = test.parent().unwrap();
305
306        // parent not owned by root
307        assert!(std::fs::metadata(parent)?.uid() != 0);
308
309        Ok(())
310    }
311
312    #[test]
313    #[ignore = "run manually"]
314    fn test_create_file() -> Result<()> {
315        // Run this test manually, steps:
316
317        /* For Unix
318            1. Run the below shell script in an empty dir with standard privileges.
319            2. Set env var 'HF_XET_TEST_PATH' to this path.
320            3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'.
321            4. Locate the path to the executable as TEST_EXE
322            5. Run test with a non-root user: 'TEST_EXE config::permission::test::test_create_file --exact --nocapture --include-ignored'
323
324        sudo mkdir rootdir
325        sudo touch rootdir/file
326         */
327
328        let test_path = std::env::var("HF_XET_TEST_PATH")?;
329        std::env::set_current_dir(test_path)?;
330        let permission = PrivilegedExecutionContext::current();
331
332        let test1 = Path::new("rootdir/regdir1/regdir2/file");
333
334        assert!(permission.create_file(test1).is_err());
335        unsafe { assert!(WARNING_PRINTED) };
336
337        unsafe { WARNING_PRINTED = false };
338
339        let test2 = Path::new("rootdir/file");
340        assert!(permission.create_file(test2).is_err());
341        unsafe { assert!(WARNING_PRINTED) };
342
343        Ok(())
344    }
345
346    #[test]
347    #[ignore = "run manually"]
348    fn test_create_file_sudo() -> Result<()> {
349        // Run this test manually, steps:
350
351        /* For Unix
352            1. Run the below shell script in an empty dir with standard privileges.
353            2. Set env var 'HF_XET_TEST_PATH' to this path.
354            3. Build the test executable by running 'cargo test -p gitxetcore --lib --no-run'.
355            4. Locate the path to the executable as TEST_EXE
356            5. Run test with a non-root user: 'sudo -E TEST_EXE config::permission::test::test_create_file_sudo --exact --nocapture --include-ignored'
357            mkdir regdir
358        */
359
360        let test_path = std::env::var("HF_XET_TEST_PATH")?;
361        std::env::set_current_dir(test_path)?;
362
363        let test = Path::new("regdir/regdir1/regdir2/file");
364
365        let permission = PrivilegedExecutionContext::current();
366        permission.create_file(test)?;
367
368        assert!(test.exists());
369
370        // not owned by root
371        assert!(std::fs::metadata(test)?.uid() != 0);
372
373        let parent = test.parent().unwrap();
374
375        // parent not owned by root
376        assert!(std::fs::metadata(parent)?.uid() != 0);
377
378        Ok(())
379    }
380}