elb_dl/
relocate.rs

1use std::collections::HashMap;
2use std::collections::VecDeque;
3use std::ffi::CString;
4use std::ffi::OsStr;
5use std::fs::Permissions;
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::PermissionsExt;
8use std::path::Path;
9use std::path::PathBuf;
10
11use elb::DynamicTag;
12use elb::Elf;
13use elb::ElfPatcher;
14
15use crate::fs;
16use crate::fs::os::unix::fs::symlink;
17use crate::DependencyTree;
18use crate::DynamicLoader;
19use crate::Error;
20
21/// Relocates ELF together with its dependencies.
22pub struct ElfRelocator {
23    loader: DynamicLoader,
24}
25
26impl ElfRelocator {
27    /// Create new relocator that uses the specified dynamic loader.
28    pub fn new(loader: DynamicLoader) -> Self {
29        Self { loader }
30    }
31
32    /// Relocates ELF `file` to `directory` together with its dependencies.
33    ///
34    /// Each ELF is copied to the subdirectory which name is BASE32-encoded hash of the file. The
35    /// dependencies are then symlinked into this directory. Each ELF's `RUNPATH` is
36    /// set to the containing directory. Each ELF's interpreter is changed to point to the interpreter from that
37    /// directory. All executables are symlinked into `directory/bin`.
38    pub fn relocate<P1: Into<PathBuf>, P2: AsRef<Path>>(
39        &self,
40        file: P1,
41        directory: P2,
42    ) -> Result<PathBuf, Error> {
43        let file = file.into();
44        let directory = directory.as_ref();
45        let mut tree = DependencyTree::new();
46        let mut queue = VecDeque::new();
47        queue.push_back(file.clone());
48        while let Some(file) = queue.pop_front() {
49            let dependencies = self.loader.resolve_dependencies(&file, &mut tree)?;
50            queue.extend(dependencies);
51        }
52        let mut hashes = HashMap::with_capacity(tree.len());
53        for (dependent, _dependencies) in tree.iter() {
54            let (hash, new_path) = relocate_file(dependent, directory)?;
55            patch_file(&new_path, directory, &hash, self.loader.page_size)?;
56            // TODO The hash is not updated after patching.
57            hashes.insert(dependent.clone(), hash);
58        }
59        for (dependent, dependencies) in tree.iter() {
60            let hash = hashes.get(dependent).expect("Inserted above");
61            let dir = directory.join(hash.as_str());
62            for dep in dependencies.iter() {
63                let file_name = dep.file_name().expect("File name exists");
64                let dep_hash = hashes.get(dep).expect("Inserted above");
65                let source = {
66                    let mut path = PathBuf::new();
67                    path.push("..");
68                    path.push(dep_hash.as_str());
69                    path.push(file_name);
70                    path
71                };
72                let target = dir.join(file_name);
73                let _ = std::fs::remove_file(&target);
74                symlink(&source, &target)?;
75            }
76        }
77        let mut new_path = PathBuf::new();
78        new_path.push(directory);
79        new_path.push(hashes.get(&file).expect("Inserted above").as_str());
80        new_path.push(file.file_name().expect("File name exists"));
81        Ok(new_path)
82    }
83}
84
85fn relocate_file(file: &Path, dir: &Path) -> Result<(Hash, PathBuf), Error> {
86    let hash = {
87        let mut file = fs::File::open(file)?;
88        let mut hasher = Blake2bHasher::new();
89        std::io::copy(&mut file, &mut hasher)?;
90        let hash = hasher.finalize();
91        let mut encoded_hash: HashArray = [0_u8; base32_fs::encoded_len(32)];
92        base32_fs::encode(&hash[..], &mut &mut encoded_hash[..]);
93        Hash(encoded_hash)
94    };
95    let mut new_path = PathBuf::new();
96    new_path.push(dir);
97    new_path.push(hash.as_str());
98    fs::create_dir_all(&new_path)?;
99    new_path.push(file.file_name().expect("File name exists"));
100    let _ = std::fs::remove_file(&new_path);
101    fs::copy(file, &new_path)?;
102    Ok((hash, new_path))
103}
104
105fn patch_file(file: &Path, directory: &Path, hash: &Hash, page_size: u64) -> Result<(), Error> {
106    let dir = file.parent().expect("Parent directory exists");
107    let dir_bytes = dir.as_os_str().as_bytes();
108    let file_name = file.file_name().expect("File name exists");
109    let Some(file_kind) = get_file_kind(file, page_size)? else {
110        // Don't patch weird files.
111        return Ok(());
112    };
113    let mode = match file_kind {
114        FileKind::Executable | FileKind::Static => 0o755,
115        FileKind::Library => 0o644,
116    };
117    fs::set_permissions(file, Permissions::from_mode(mode))?;
118    if matches!(file_kind, FileKind::Executable | FileKind::Static) {
119        let bin = directory.join("bin");
120        fs::create_dir_all(&bin)?;
121        let source = {
122            let mut path = PathBuf::new();
123            path.push("..");
124            path.push(hash.as_str());
125            path.push(file_name);
126            path
127        };
128        let target = bin.join(file_name);
129        let _ = std::fs::remove_file(&target);
130        symlink(&source, &target)?;
131    }
132    if file_kind == FileKind::Static {
133        // Don't patch statically-linked executables.
134        return Ok(());
135    }
136    let mut file = fs::OpenOptions::new().read(true).write(true).open(file)?;
137    let elf = Elf::read(&mut file, page_size)?;
138    let mut patcher = ElfPatcher::new(elf, file);
139    if let Some(old_interpreter) = patcher.read_interpreter()? {
140        let interpreter = {
141            let old_interpreter = Path::new(OsStr::from_bytes(old_interpreter.to_bytes()));
142            let file_name_bytes = old_interpreter
143                .file_name()
144                .expect("File name exists")
145                .as_bytes();
146            let mut bytes = Vec::with_capacity(dir_bytes.len() + file_name_bytes.len() + 2);
147            bytes.extend_from_slice(dir_bytes);
148            bytes.extend_from_slice(std::path::MAIN_SEPARATOR_STR.as_bytes());
149            bytes.extend_from_slice(file_name_bytes);
150            bytes.push(0_u8);
151            // SAFETY: The string is constructed from `dir` and `file_name` which don't contain NUL
152            // characters. We add a NUL character at the end.
153            unsafe { CString::from_vec_with_nul_unchecked(bytes) }
154        };
155        patcher.set_interpreter(interpreter.as_c_str())?;
156    }
157    let runpath = {
158        let mut bytes = Vec::with_capacity(dir_bytes.len() + 1);
159        bytes.extend_from_slice(dir_bytes);
160        bytes.push(0_u8);
161        // SAFETY: The string is constructed from `dir` which doesn't contain NUL characters. We
162        // add a NUL character at the end.
163        unsafe { CString::from_vec_with_nul_unchecked(bytes) }
164    };
165    patcher.set_dynamic_tag(DynamicTag::Runpath, runpath.as_c_str())?;
166    patcher.finish()?;
167    Ok(())
168}
169
170fn get_file_kind(file: &Path, page_size: u64) -> Result<Option<FileKind>, Error> {
171    let mut file = fs::File::open(file)?;
172    let elf = Elf::read(&mut file, page_size)?;
173    if elf.read_interpreter(&mut file)?.is_some() {
174        return Ok(Some(FileKind::Executable));
175    }
176    // No interpreter, but may be this is statically-linked executable.
177    let Some(dynamic_table) = elf.read_dynamic_table(&mut file)? else {
178        return Ok(None);
179    };
180    Ok(match dynamic_table.get(DynamicTag::Needed) {
181        Some(..) => Some(FileKind::Library),
182        None => Some(FileKind::Static),
183    })
184}
185
186#[derive(Clone, Copy, PartialEq, Eq, Debug)]
187enum FileKind {
188    Executable,
189    Library,
190    Static,
191}
192
193struct Blake2bHasher {
194    state: blake2b_simd::State,
195}
196
197impl Blake2bHasher {
198    fn new() -> Self {
199        Self {
200            state: blake2b_simd::Params::new().hash_length(32).to_state(),
201        }
202    }
203
204    fn finalize(self) -> [u8; 32] {
205        self.state
206            .finalize()
207            .as_bytes()
208            .try_into()
209            .expect("64 > 32")
210    }
211}
212
213impl std::io::Write for Blake2bHasher {
214    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
215        self.state.update(data);
216        Ok(data.len())
217    }
218
219    fn flush(&mut self) -> std::io::Result<()> {
220        Ok(())
221    }
222}
223
224type HashArray = [u8; base32_fs::encoded_len(32)];
225
226struct Hash(HashArray);
227
228impl Hash {
229    fn as_str(&self) -> &str {
230        // SAFETY: BASE32 string is composed of ASCII characters. Any ASCII string is also a valid
231        // UTF-8 string.
232        unsafe { std::str::from_utf8_unchecked(&self.0[..]) }
233    }
234}