From 889ffc9880b80aa6638e32bb5bb6cf195c558b23 Mon Sep 17 00:00:00 2001 From: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:03:29 +0200 Subject: [PATCH] ohos: Fix JIT detection (#40130) On recent versions of HarmonyOS the previous JIT detection does not appear to work anymore, since the second `mmap` succeeds. This moves the failure in spidermonkey to a later point, when SM remaps memory to be writable. On recent versions of HOS I observed that the `PROT_WRITE` permission was silently ignored by mprotect, which lead to a crash later on the first attempt to write to the memory region. It's not exactly easy to determine if one can write to a memory location (if mprotect lies to you), but we can use `read` to check if the memory region is writable (without triggering a segfault), since `read` will return an error for invalid addresses (which is possible since the writing is handled in the kernel). Since this solution does add more unsafe code than before, we only use this detection on OpenHarmony, although we could use it on more platforms later if there are other platforms which also may have JIT forbidden. Testing: Removing the explicit `--pref js_disable_jit=true` means the detection is tested in CI. Fixes: #40029 --------- Signed-off-by: Jonathan Schwender Signed-off-by: Jonathan Schwender <55576758+jschwe@users.noreply.github.com> Co-authored-by: Sam <16504129+sagudev@users.noreply.github.com> --- .github/workflows/ohos.yml | 4 +- components/script/init.rs | 86 ++++++++++++++++++++++++++++---- python/servo/testing_commands.py | 2 - 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ohos.yml b/.github/workflows/ohos.yml index 5115ce52c37..7562ddf82ce 100644 --- a/.github/workflows/ohos.yml +++ b/.github/workflows/ohos.yml @@ -215,8 +215,8 @@ jobs: # The main reason however, is that we can use the application traces to determine if servo # successfully reaches certain locations in the code, in particular if a page is successfully loaded. hdc shell hitrace -b "${TRACE_BUFFER_SZ_KB}" app graphic ohos freq idle memory --trace_begin - # We start servo, tell it to load a website (servo.org). JIT is not allowed on HarmonyOS 5. - hdc shell aa start -a EntryAbility -b org.servo.servo -U https://servo.org --ps=--pref js_disable_jit=true + # We start servo, tell it to load a website (servo.org). + hdc shell aa start -a EntryAbility -b org.servo.servo -U https://servo.org servo_pid=$(hdc shell pidof org.servo.servo) # We don't really know how long servo needs to load a webpage, so we just wait 10s. sleep 10 diff --git a/components/script/init.rs b/components/script/init.rs index b7ebbbe47a1..bf4fbae670f 100644 --- a/components/script/init.rs +++ b/components/script/init.rs @@ -1,7 +1,6 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - use js::jsapi::JSObject; use servo_config::pref; @@ -63,34 +62,99 @@ unsafe extern "C" fn is_dom_object(obj: *mut JSObject) -> bool { /// /// Spidermonkey will crash if JIT is not allowed on a system, so we do a short detection /// if jit is allowed or not. -#[cfg(target_os = "linux")] +/// +/// Note: This implementation should work fine on all Linux systems, perhaps even Unix systems, +/// but for now we only enable it on OpenHarmony, since that is where it is most needed. +#[cfg(target_env = "ohos")] #[allow(unsafe_code)] fn jit_forbidden() -> bool { + debug!("Testing if JIT is allowed."); + + fn mem_is_writable(ptr: *mut core::ffi::c_void) -> std::io::Result { + debug!("Testing if ptr {ptr:?} is writable"); + // Safety: This is cursed, but we can use read to determine if ptr + // can be written to. `read` is a syscall and will return an error code + // if ptr can't be written (instead of a segfault as with a regular access). + // We also take care to always close `fd`. + #[allow(unsafe_code)] + unsafe { + let fd = libc::open(c"/dev/zero".as_ptr(), libc::O_RDONLY); + if fd < 0 { + return Err(std::io::Error::last_os_error()); + } + let writable = libc::read(fd, ptr, 1) > 0; + if !writable { + debug!( + "addr is not writable. Error: {}", + std::io::Error::last_os_error() + ); + } + libc::close(fd); + Ok(writable) + } + } + + // We need to allocate at least one page, so we query the page size on the system. + let map_size: libc::size_t = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as libc::size_t }; let flags = libc::MAP_NORESERVE | libc::MAP_PRIVATE | libc::MAP_ANON; - let first_mmap = - unsafe { libc::mmap(core::ptr::null_mut(), 4096, libc::PROT_NONE, flags, -1, 0) }; + // SAFETY: We mmap one anonymous page, with no special flags, so this has no safety + // implications. + let first_mmap = unsafe { + libc::mmap( + core::ptr::null_mut(), + map_size, + libc::PROT_NONE, + flags, + -1, + 0, + ) + }; assert_ne!(first_mmap, libc::MAP_FAILED, "mmap not allowed?"); + let remap_flags = libc::MAP_ANONYMOUS | libc::MAP_FIXED | libc::MAP_PRIVATE | libc::MAP_EXECUTABLE; // remap the page with PROT_EXEC. If this fails, JIT is not possible. let second_mmap = unsafe { libc::mmap( first_mmap, - 4096, + map_size, libc::PROT_READ | libc::PROT_EXEC, remap_flags, -1, 0, ) }; - let remap_failed = second_mmap == libc::MAP_FAILED; - // SAFETY: For the second mmap we used `MAP_FIXED` so in both success and failure case, the - // address will be the same as the first mmap. - unsafe { libc::munmap(first_mmap, 4096) }; - remap_failed + let mut jit_forbidden = second_mmap == libc::MAP_FAILED; + if !jit_forbidden { + // Spidermonkey uses mprotect to make the memory writable. + // SAFETY: We obtained the memory in question via `mmap` and are not using the memory + // in any way. + let res = + unsafe { libc::mprotect(first_mmap, map_size, libc::PROT_READ | libc::PROT_WRITE) }; + if res != 0 { + // `mprotect` failed (to add write permissions), so we presume it is because JIT is forbidden. + jit_forbidden = true; + } else { + // Additionally check if `mprotect` actually succeeded in adding `PROT_WRITE`. + // We observed before that `mprotect` silently ignores the write permission without + // returning an error. + let is_writable = mem_is_writable(first_mmap) + .inspect_err(|_e| { + debug!("Failed to determine if JIT is allowed. Conservatively assuming it is forbidden."); + }) + .unwrap_or(false); // writable == false -> JIT is forbidden. + jit_forbidden = !is_writable; + } + } + // Ignore the result, since there is nothing we could do if unmap failed for whatever reason. + // SAFETY: We unmap the `mmap`ed region completely again. There is no other `munmap` call in + // this function, and we do not have any early returns in this function. + let _ = unsafe { libc::munmap(first_mmap, map_size) }; + + jit_forbidden } -#[cfg(not(target_os = "linux"))] +#[cfg(not(target_env = "ohos"))] fn jit_forbidden() -> bool { false } diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 42b47e93cc4..8762f52e7e3 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -761,8 +761,6 @@ class MachCommands(CommandBase): "org.servo.servo", "-U", "https://servospeedometer.netlify.app?headless=1", - "--ps=--pref", - "js_disable_jit=true", "--ps", "--log-filter", "script::dom::console",