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