incular_core/
hot_reload.rs1#[inline]
16pub fn call<F, R>(callback: &mut F) -> R
17where
18 F: FnMut() -> R,
19{
20 #[cfg(feature = "hot-reload")]
21 {
22 fn entry<F, R>(callback: &mut F) -> R
26 where
27 F: FnMut() -> R,
28 {
29 callback()
30 }
31 subsecond::HotFn::current(entry::<F, R>).call((callback,))
32 }
33 #[cfg(not(feature = "hot-reload"))]
34 {
35 callback()
36 }
37}
38
39#[inline]
42pub fn call_with<F, A, R>(callback: &mut F, argument: A) -> R
43where
44 F: FnMut(A) -> R,
45{
46 #[cfg(feature = "hot-reload")]
47 {
48 fn entry<F, A, R>(callback: &mut F, argument: A) -> R
49 where
50 F: FnMut(A) -> R,
51 {
52 callback(argument)
53 }
54 subsecond::HotFn::current(entry::<F, A, R>).call((callback, argument))
55 }
56 #[cfg(not(feature = "hot-reload"))]
57 {
58 callback(argument)
59 }
60}
61
62#[cfg(feature = "hot-reload")]
66#[must_use]
67pub fn aslr_reference() -> u64 {
68 subsecond::aslr_reference() as u64
69}
70
71#[cfg(feature = "hot-reload")]
73#[derive(Clone, Debug, serde::Deserialize)]
74#[serde(transparent)]
75pub struct HotPatch(subsecond::JumpTable);
76
77#[cfg(feature = "hot-reload")]
78impl HotPatch {
79 #[must_use]
81 pub fn function_count(&self) -> usize {
82 self.0.map.len()
83 }
84
85 pub unsafe fn apply(self) -> Result<(), HotPatchError> {
95 #[cfg(all(windows, target_arch = "x86_64"))]
96 let table = {
97 let mut table = self.0;
98 table.lib = library_with_safe_trampolines(&table.lib)?;
99 table
100 };
101 #[cfg(not(all(windows, target_arch = "x86_64")))]
102 let table = self.0;
103 unsafe { subsecond::apply_patch(table) }.map_err(|error| HotPatchError(error.to_string()))
105 }
106}
107
108#[cfg(feature = "hot-reload")]
109const PE_MACHINE_X86_64: u16 = 0x8664;
110#[cfg(feature = "hot-reload")]
111const PE_SECTION_EXECUTE: u32 = 0x2000_0000;
112#[cfg(feature = "hot-reload")]
113const PE_SECTION_HEADER_SIZE: usize = 40;
114
115#[cfg(feature = "hot-reload")]
130pub fn retarget_trampolines(image: &mut [u8]) -> usize {
131 let mut rewritten = 0;
132 for (start, end) in executable_sections(image) {
133 let mut offset = start;
134 while offset + 13 <= end {
136 let code = &mut image[offset..offset + 13];
137 if code[..2] == [0x48, 0xB8]
138 && code[10..12] == [0xFF, 0xE0]
139 && matches!(code[12], 0x00 | 0xCC)
140 {
141 code[..2].copy_from_slice(&[0x49, 0xBB]);
142 code[10..].copy_from_slice(&[0x41, 0xFF, 0xE3]);
143 rewritten += 1;
144 offset += 13;
145 } else {
146 offset += 1;
147 }
148 }
149 }
150 rewritten
151}
152
153#[cfg(feature = "hot-reload")]
155fn executable_sections(image: &[u8]) -> Vec<(usize, usize)> {
156 let u16_at = |offset: usize| {
157 let bytes = image.get(offset..offset.checked_add(2)?)?;
158 Some(u16::from_le_bytes([bytes[0], bytes[1]]))
159 };
160 let u32_at = |offset: usize| {
161 let bytes = image.get(offset..offset.checked_add(4)?)?;
162 Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
163 };
164 let sections = || {
165 let header = usize::try_from(u32_at(0x3C)?).ok()?;
166 if image.get(header..header.checked_add(4)?)? != b"PE\0\0" {
167 return None;
168 }
169 let coff = header + 4;
170 if u16_at(coff)? != PE_MACHINE_X86_64 {
171 return None;
172 }
173 let count = usize::from(u16_at(coff + 2)?);
174 let table = coff + 20 + usize::from(u16_at(coff + 16)?);
175 let mut ranges = Vec::new();
176 for index in 0..count {
177 let section = table + index * PE_SECTION_HEADER_SIZE;
178 let size = usize::try_from(u32_at(section + 16)?).ok()?;
179 let start = usize::try_from(u32_at(section + 20)?).ok()?;
180 if u32_at(section + 36)? & PE_SECTION_EXECUTE != 0 {
181 ranges.push((
182 start.min(image.len()),
183 start.saturating_add(size).min(image.len()),
184 ));
185 }
186 }
187 Some(ranges)
188 };
189 sections().unwrap_or_default()
190}
191
192#[cfg(all(feature = "hot-reload", windows, target_arch = "x86_64"))]
196fn library_with_safe_trampolines(
197 library: &std::path::Path,
198) -> Result<std::path::PathBuf, HotPatchError> {
199 const SUFFIX: &str = "-incular.dll";
200 let error = |error: std::io::Error| HotPatchError(format!("{}: {error}", library.display()));
201 let mut image = std::fs::read(library).map_err(error)?;
202 if retarget_trampolines(&mut image) == 0 {
203 return Ok(library.to_path_buf());
204 }
205 let stem = library.file_stem().unwrap_or_default().to_string_lossy();
206 let repaired = library.with_file_name(format!("{stem}{SUFFIX}"));
207 if let Some(entries) = library.parent().and_then(|parent| parent.read_dir().ok()) {
210 for entry in entries.flatten() {
211 if entry.file_name().to_string_lossy().ends_with(SUFFIX) {
212 let _ = std::fs::remove_file(entry.path());
213 }
214 }
215 }
216 std::fs::write(&repaired, image).map_err(error)?;
217 Ok(repaired)
218}
219
220#[cfg(feature = "hot-reload")]
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct HotPatchError(String);
224
225#[cfg(feature = "hot-reload")]
226impl std::fmt::Display for HotPatchError {
227 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(formatter, "hot patch was not applied: {}", self.0)
229 }
230}
231
232#[cfg(feature = "hot-reload")]
233impl std::error::Error for HotPatchError {}