Skip to main content

ghidra_ipc/
owner_only.rs

1//! Owner-only directory lockdown: the Unix `chmod 0700` equivalent on both
2//! platforms. Single home for the DACL/`chmod` logic so the two callers
3//! (`ghidra-mcp::logging` for the log dir, `ghidra-worker-ctl::boot` for the
4//! per-boot token dir) cannot drift apart.
5
6use std::io;
7use std::path::Path;
8
9/// Restrict `path` (a directory) to the current user. On Unix this is a real
10/// `0700`; on Windows it replaces the DACL with a single owner-only ACE and
11/// marks it protected so no inherited `Everyone`/`BUILTIN\Users` grant
12/// survives. Mirrors `chmod 0700`.
13pub fn restrict_to_owner(path: &Path) -> io::Result<()> {
14    #[cfg(unix)]
15    {
16        use std::os::unix::fs::PermissionsExt;
17        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
18    }
19    #[cfg(windows)]
20    {
21        windows_acl_impl::apply(path)
22    }
23    #[cfg(not(any(unix, windows)))]
24    {
25        let _ = path;
26        Err(io::Error::other(
27            "owner-only lockdown not implemented on this platform",
28        ))
29    }
30}
31
32#[cfg(windows)]
33mod windows_acl_impl {
34    use std::io;
35    use std::path::Path;
36
37    use windows::core::{Error as WinError, PWSTR};
38    use windows::Win32::Foundation::LocalFree;
39    use windows::Win32::Foundation::{CloseHandle, ERROR_SUCCESS, HANDLE, HLOCAL};
40    use windows::Win32::Security::Authorization::{
41        SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, NO_MULTIPLE_TRUSTEE,
42        SET_ACCESS, SE_FILE_OBJECT, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
43    };
44    use windows::Win32::Security::{
45        GetTokenInformation, TokenUser, ACL, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
46        PROTECTED_DACL_SECURITY_INFORMATION, TOKEN_QUERY, TOKEN_USER,
47    };
48    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
49
50    /// FILE_ALL_ACCESS (winnt.h): STANDARD_RIGHTS_ALL | FILE_GENERIC_READ | FILE_GENERIC_WRITE |
51    /// FILE_GENERIC_EXECUTE | FILE_DELETE_CHILD. Hardcoded to avoid pulling in the whole
52    /// Win32_Storage_FileSystem feature area for one constant.
53    const FILE_ALL_ACCESS: u32 = 0x1F01FF;
54
55    /// Apply a DACL to `path` that grants FILE_ALL_ACCESS to the current process's user SID only,
56    /// and strips all inherited ACEs (PROTECTED_DACL_SECURITY_INFORMATION) so no ambient
57    /// `Everyone`/`BUILTIN\Users` grant survives. Mirrors `chmod 0700` on Unix.
58    pub fn apply(path: &Path) -> io::Result<()> {
59        // 1. Open the current process token (query-only — we just need the user SID).
60        let mut token = HANDLE::default();
61        unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }
62            .map_err(win_err("OpenProcessToken"))?;
63        let _token_guard = HandleGuard(token);
64
65        // 2. GetTokenInformation(TokenUser) — two-call pattern: probe size, then fetch.
66        let mut needed: u32 = 0;
67        // SAFETY: null buffer + 0 size is the documented size-probe call; it always "fails" with
68        // ERROR_INSUFFICIENT_BUFFER while writing the required size into `needed`.
69        if let Err(e) = unsafe { GetTokenInformation(token, TokenUser, None, 0, &mut needed) } {
70            if needed == 0 {
71                return Err(win_err("GetTokenInformation (size probe)")(e));
72            }
73        }
74        let mut buf = vec![0u8; needed as usize];
75        let mut written: u32 = 0;
76        unsafe {
77            GetTokenInformation(
78                token,
79                TokenUser,
80                Some(buf.as_mut_ptr() as *mut core::ffi::c_void),
81                needed,
82                &mut written,
83            )
84        }
85        .map_err(win_err("GetTokenInformation"))?;
86        // SAFETY: `buf` was sized/filled by the successful call above per the TOKEN_USER layout.
87        // `buf` (Vec<u8>) is only 1-byte aligned, so dereferencing it as TOKEN_USER would be
88        // unaligned UB — copy it out with `read_unaligned` instead.
89        let token_user: TOKEN_USER =
90            unsafe { std::ptr::read_unaligned(buf.as_ptr() as *const TOKEN_USER) };
91        let sid = token_user.User.Sid;
92
93        // 3. Build a single EXPLICIT_ACCESS_W entry granting FILE_ALL_ACCESS to that SID, replacing
94        //    (not merging with) any existing ACE for the same trustee.
95        let trustee = TRUSTEE_W {
96            pMultipleTrustee: std::ptr::null_mut(),
97            MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
98            TrusteeForm: TRUSTEE_IS_SID,
99            TrusteeType: TRUSTEE_IS_USER,
100            ptstrName: PWSTR(sid.0 as *mut u16),
101        };
102
103        let ea = EXPLICIT_ACCESS_W {
104            grfAccessPermissions: FILE_ALL_ACCESS,
105            grfAccessMode: SET_ACCESS,
106            grfInheritance: NO_INHERITANCE,
107            Trustee: trustee,
108        };
109
110        // 4. SetEntriesInAclW allocates a brand-new ACL (owner-only, no prior entries merged in).
111        let mut new_acl: *mut ACL = std::ptr::null_mut();
112        let rc = unsafe { SetEntriesInAclW(Some(&[ea]), None, &mut new_acl) };
113        if rc != ERROR_SUCCESS {
114            return Err(io::Error::other(format!("SetEntriesInAclW failed: {rc:?}")));
115        }
116        let _acl_guard = LocalFreeGuard(new_acl as *mut core::ffi::c_void);
117
118        // 5. Apply the new DACL to the path and, critically, mark it PROTECTED so no inherited ACEs
119        //    (Everyone / BUILTIN\Users from the parent dir) survive alongside it.
120        let wide_path = to_wide_null(path);
121        let rc = unsafe {
122            SetNamedSecurityInfoW(
123                PWSTR(wide_path.as_ptr() as *mut u16),
124                SE_FILE_OBJECT,
125                DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
126                None,
127                None,
128                Some(new_acl),
129                None,
130            )
131        };
132        if rc != ERROR_SUCCESS {
133            return Err(io::Error::other(format!(
134                "SetNamedSecurityInfoW failed: {rc:?}"
135            )));
136        }
137
138        Ok(())
139    }
140
141    fn win_err(ctx: &'static str) -> impl Fn(WinError) -> io::Error {
142        move |e| io::Error::other(format!("{ctx}: {e}"))
143    }
144
145    fn to_wide_null(path: &Path) -> Vec<u16> {
146        use std::os::windows::ffi::OsStrExt;
147        path.as_os_str()
148            .encode_wide()
149            .chain(std::iter::once(0))
150            .collect()
151    }
152
153    /// RAII close for the process token handle.
154    struct HandleGuard(HANDLE);
155    impl Drop for HandleGuard {
156        fn drop(&mut self) {
157            // SAFETY: handle came from a successful OpenProcessToken owned solely by this guard.
158            unsafe {
159                let _ = CloseHandle(self.0);
160            }
161        }
162    }
163
164    /// RAII free for the LocalAlloc'd ACL returned by `SetEntriesInAclW`, per its documented
165    /// contract (caller must LocalFree the new ACL when done with it).
166    struct LocalFreeGuard(*mut core::ffi::c_void);
167    impl Drop for LocalFreeGuard {
168        fn drop(&mut self) {
169            if !self.0.is_null() {
170                // SAFETY: pointer was allocated by SetEntriesInAclW (LocalAlloc-backed) and not
171                // freed elsewhere.
172                //
173                // `windows` 0.62 takes `Option<HLOCAL>` here where 0.58 took a bare `HLOCAL`;
174                // `None` is the API's documented no-op, and the null check above already
175                // covers that case, so this is always `Some`.
176                unsafe {
177                    let _ = LocalFree(Some(HLOCAL(self.0)));
178                }
179            }
180        }
181    }
182}