| ... | ... | @@ -0,0 +1,264 @@ |
| 1 | //! Starting on every Linux, NixOS included. Releases carry no PT_INTERP (`linux/package.sh` |
| 2 | //! clears it), so the kernel starts them at `sb_entry` as it would a static executable; |
| 3 | //! stage 1 then re-executes the file under the dynamic loader `/bin/sh` uses. Started by a |
| 4 | //! dynamic loader, `sb_entry` hands straight to glibc's `_start`. |
| 5 | |
| 6 | use std::{ |
| 7 | ffi::{CStr, CString, OsStr, c_void}, |
| 8 | io, |
| 9 | mem::MaybeUninit, |
| 10 | os::unix::ffi::OsStrExt, |
| 11 | path::{Path, PathBuf}, |
| 12 | }; |
| 13 | |
| 14 | // A dynamic loader passes its finalizer to `_start` in x0 or rdx; the kernel passes zero. |
| 15 | #[cfg(target_arch = "aarch64")] |
| 16 | std::arch::global_asm!( |
| 17 | ".globl sb_entry", |
| 18 | ".type sb_entry, %function", |
| 19 | "sb_entry:", |
| 20 | "cbz x0, 1f", |
| 21 | "b _start", |
| 22 | "1: mov x0, sp", |
| 23 | "b {stage1}", |
| 24 | stage1 = sym stage1, |
| 25 | ); |
| 26 | #[cfg(target_arch = "x86_64")] |
| 27 | std::arch::global_asm!( |
| 28 | ".globl sb_entry", |
| 29 | ".type sb_entry, @function", |
| 30 | "sb_entry:", |
| 31 | "test rdx, rdx", |
| 32 | "jnz _start", |
| 33 | "mov rdi, rsp", |
| 34 | "and rsp, -16", |
| 35 | "call {stage1}", |
| 36 | stage1 = sym stage1, |
| 37 | ); |
| 38 | |
| 39 | #[cfg(target_arch = "aarch64")] |
| 40 | mod call { |
| 41 | pub const OPENAT: usize = 56; |
| 42 | pub const PREAD: usize = 67; |
| 43 | pub const READLINKAT: usize = 78; |
| 44 | pub const EXECVE: usize = 221; |
| 45 | pub const EXIT: usize = 93; |
| 46 | pub const WRITE: usize = 64; |
| 47 | pub const MMAP: usize = 222; |
| 48 | |
| 49 | pub unsafe fn syscall(number: usize, a: usize, b: usize, c: usize, d: usize) -> isize { |
| 50 | let result; |
| 51 | unsafe { |
| 52 | std::arch::asm!("svc 0", in("x8") number, inlateout("x0") a => result, in("x1") b, |
| 53 | in("x2") c, in("x3") d, in("x4") usize::MAX, in("x5") 0, options(nostack)) |
| 54 | }; |
| 55 | result |
| 56 | } |
| 57 | } |
| 58 | #[cfg(target_arch = "x86_64")] |
| 59 | mod call { |
| 60 | pub const OPENAT: usize = 257; |
| 61 | pub const PREAD: usize = 17; |
| 62 | pub const READLINKAT: usize = 267; |
| 63 | pub const EXECVE: usize = 59; |
| 64 | pub const EXIT: usize = 60; |
| 65 | pub const WRITE: usize = 1; |
| 66 | pub const MMAP: usize = 9; |
| 67 | |
| 68 | pub unsafe fn syscall(number: usize, a: usize, b: usize, c: usize, d: usize) -> isize { |
| 69 | let result; |
| 70 | unsafe { |
| 71 | std::arch::asm!("syscall", inlateout("rax") number => result, in("rdi") a, |
| 72 | in("rsi") b, in("rdx") c, in("r10") d, in("r8") usize::MAX, in("r9") 0, |
| 73 | lateout("rcx") _, lateout("r11") _, options(nostack)) |
| 74 | }; |
| 75 | result |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | const CWD: usize = -100isize as usize; |
| 80 | const O_CLOEXEC: usize = 0o2000000; |
| 81 | static PROC_SELF_EXE: [u8; 15] = *b"/proc/self/exe\0"; |
| 82 | static SHELLS: [[u8; 13]; 2] = [*b"/bin/sh\0\0\0\0\0\0", *b"/usr/bin/env\0"]; |
| 83 | static NO_LOADER: [u8; 62] = *b"snowbound: no dynamic loader; Snowbound runs on glibc systems\n"; |
| 84 | |
| 85 | /// The interpreter of the ELF at `path` into `buffer`, NUL-terminated; false where it has |
| 86 | /// none. |
| 87 | unsafe fn interpreter(path: *const u8, buffer: *mut u8, capacity: usize) -> bool { |
| 88 | use call::*; |
| 89 | unsafe { |
| 90 | let file = syscall(OPENAT, CWD, path as usize, O_CLOEXEC, 0); |
| 91 | if file < 0 { |
| 92 | return false; |
| 93 | } |
| 94 | let file = file as usize; |
| 95 | let mut header = MaybeUninit::<[u64; 8]>::uninit(); |
| 96 | let header = header.as_mut_ptr() as *mut u8; |
| 97 | if syscall(PREAD, file, header as usize, 64, 0) != 64 |
| 98 | || (header as *const u32).read_unaligned() != u32::from_le_bytes(*b"\x7fELF") |
| 99 | { |
| 100 | return false; |
| 101 | } |
| 102 | let table = (header.add(32) as *const u64).read_unaligned() as usize; |
| 103 | let size = (header.add(54) as *const u16).read_unaligned() as usize; |
| 104 | let count = (header.add(56) as *const u16).read_unaligned() as usize; |
| 105 | let mut entry = MaybeUninit::<[u64; 7]>::uninit(); |
| 106 | let entry = entry.as_mut_ptr() as *mut u8; |
| 107 | let mut index = 0; |
| 108 | while index < count { |
| 109 | if syscall(PREAD, file, entry as usize, 56, table + index * size) == 56 |
| 110 | && (entry as *const u32).read_unaligned() == 3 |
| 111 | { |
| 112 | let offset = (entry.add(8) as *const u64).read_unaligned() as usize; |
| 113 | let length = (entry.add(32) as *const u64).read_unaligned() as usize; |
| 114 | return length < capacity |
| 115 | && syscall(PREAD, file, buffer as usize, length, offset) == length as isize |
| 116 | && { |
| 117 | buffer.add(length).write_volatile(0); |
| 118 | true |
| 119 | }; |
| 120 | } |
| 121 | index += 1; |
| 122 | } |
| 123 | false |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | /// Runs before relocation: only system calls, the stack and position-relative statics. |
| 128 | unsafe extern "C" fn stage1(stack: *const usize) -> ! { |
| 129 | use call::*; |
| 130 | unsafe { |
| 131 | let argc = *stack; |
| 132 | let argv = stack.add(1); |
| 133 | let environment = argv.add(argc + 1); |
| 134 | let mut exe = MaybeUninit::<[u8; 4096]>::uninit(); |
| 135 | let exe = exe.as_mut_ptr() as *mut u8; |
| 136 | let mut loader = MaybeUninit::<[u8; 4096]>::uninit(); |
| 137 | let loader = loader.as_mut_ptr() as *mut u8; |
| 138 | let length = syscall( |
| 139 | READLINKAT, |
| 140 | CWD, |
| 141 | PROC_SELF_EXE.as_ptr() as usize, |
| 142 | exe as usize, |
| 143 | 4095, |
| 144 | ); |
| 145 | let found = length > 0 && { |
| 146 | exe.add(length as usize).write_volatile(0); |
| 147 | let mut shell = 0; |
| 148 | while shell < SHELLS.len() && !interpreter(SHELLS[shell].as_ptr(), loader, 4096) { |
| 149 | shell += 1; |
| 150 | } |
| 151 | shell < SHELLS.len() |
| 152 | }; |
| 153 | // The loader runs the program its `argv[1]` names, which the program gets as `argv[0]`. |
| 154 | let bytes = (argc + 2) * size_of::<usize>(); |
| 155 | let arguments = syscall(MMAP, 0, bytes, 3, 0x22) as *mut usize; |
| 156 | if found && (arguments as isize) > 0 { |
| 157 | arguments.write_volatile(loader as usize); |
| 158 | arguments.add(1).write_volatile(exe as usize); |
| 159 | let mut index = 1; |
| 160 | while index < argc { |
| 161 | arguments.add(index + 1).write_volatile(*argv.add(index)); |
| 162 | index += 1; |
| 163 | } |
| 164 | arguments.add(argc + 1).write_volatile(0); |
| 165 | syscall( |
| 166 | EXECVE, |
| 167 | loader as usize, |
| 168 | arguments as usize, |
| 169 | environment as usize, |
| 170 | 0, |
| 171 | ); |
| 172 | } |
| 173 | syscall(WRITE, 2, NO_LOADER.as_ptr() as usize, NO_LOADER.len(), 0); |
| 174 | loop { |
| 175 | syscall(EXIT, 127, 0, 0, 0); |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | /// This executable's path. `current_exe` names the dynamic loader where stage 1 or a person |
| 181 | /// started the file through it; the loader passes the file's path on as `argv[0]`. |
| 182 | pub fn executable() -> io::Result<PathBuf> { |
| 183 | let running = std::env::current_exe()?; |
| 184 | let name = running.file_name().map_or(&b""[..], |name| name.as_bytes()); |
| 185 | if !name.starts_with(b"ld-") { |
| 186 | return Ok(running); |
| 187 | } |
| 188 | std::fs::canonicalize(std::env::args_os().next().ok_or(io::ErrorKind::NotFound)?) |
| 189 | } |
| 190 | |
| 191 | /// Whether the library `name` loads, as the crates that open it would find it. |
| 192 | pub fn loads(name: &CStr) -> bool { |
| 193 | !unsafe { libc::dlopen(name.as_ptr(), libc::RTLD_LAZY) }.is_null() |
| 194 | } |
| 195 | |
| 196 | /// The libraries Snowbound and its crates open by name, Wayland's and X11's first. |
| 197 | const LIBRARIES: [&CStr; 14] = [ |
| 198 | c"libwayland-client.so.0", |
| 199 | c"libwayland-egl.so.1", |
| 200 | c"libxkbcommon.so.0", |
| 201 | c"libxkbcommon-x11.so.0", |
| 202 | c"libX11.so.6", |
| 203 | c"libX11-xcb.so.1", |
| 204 | c"libXcursor.so.1", |
| 205 | c"libXi.so.6", |
| 206 | c"libxcb.so.1", |
| 207 | c"libvulkan.so.1", |
| 208 | c"libEGL.so.1", |
| 209 | c"libfontconfig.so.1", |
| 210 | c"libenchant-2.so.2", |
| 211 | c"libgstreamer-1.0.so.0", |
| 212 | ]; |
| 213 | |
| 214 | /// On NixOS, where libraries sit only in the store, loads by path each library the system's |
| 215 | /// and the user's profiles have that no search finds; opening one by name then finds it |
| 216 | /// loaded. |
| 217 | pub fn preload() { |
| 218 | let system = Path::new("/run/current-system"); |
| 219 | if !system.exists() { |
| 220 | return; |
| 221 | } |
| 222 | let mut missing: Vec<&CStr> = LIBRARIES.into_iter().filter(|name| !loads(name)).collect(); |
| 223 | if missing.is_empty() { |
| 224 | return; |
| 225 | } |
| 226 | let home = std::env::var_os("HOME").map(PathBuf::from); |
| 227 | let user = std::env::var_os("USER").map(|user| Path::new("/etc/profiles/per-user").join(user)); |
| 228 | let profiles = [ |
| 229 | Some(system.to_owned()), |
| 230 | user, |
| 231 | home.map(|home| home.join(".nix-profile")), |
| 232 | ]; |
| 233 | let Ok(output) = std::process::Command::new(system.join("sw/bin/nix-store")) |
| 234 | .arg("--query") |
| 235 | .arg("--requisites") |
| 236 | .args( |
| 237 | profiles |
| 238 | .into_iter() |
| 239 | .flatten() |
| 240 | .filter(|profile| profile.exists()), |
| 241 | ) |
| 242 | .stderr(std::process::Stdio::null()) |
| 243 | .output() |
| 244 | else { |
| 245 | return; |
| 246 | }; |
| 247 | for path in output.stdout.split(|&byte| byte == b'\n') { |
| 248 | let folder = Path::new(OsStr::from_bytes(path)).join("lib"); |
| 249 | if missing.is_empty() { |
| 250 | break; |
| 251 | } |
| 252 | if !folder.is_dir() { |
| 253 | continue; |
| 254 | } |
| 255 | missing.retain(|name| { |
| 256 | let file = folder.join(OsStr::from_bytes(name.to_bytes())); |
| 257 | let Ok(file) = CString::new(file.as_os_str().as_bytes()) else { |
| 258 | return true; |
| 259 | }; |
| 260 | let library: *mut c_void = unsafe { libc::dlopen(file.as_ptr(), libc::RTLD_LAZY) }; |
| 261 | library.is_null() |
| 262 | }); |
| 263 | } |
| 264 | } |