diff --git a/.gitattributes b/.gitattributes index 8945d16..8137aca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ # Preserve exact upstream whitespace in the provenance-locked Android core # snapshot. Local Mocktail sources remain subject to git diff --check. third_party/mcpelauncher-linker/core/** -whitespace +packaging/freebsd/*.patch -whitespace diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index e01c6ee..10834a7 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -53,7 +53,7 @@ jobs: export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install --yes --no-install-recommends \ - build-essential cmake git ninja-build pkg-config \ + build-essential cmake git ninja-build pkg-config lld \ libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev \ libssl-dev nlohmann-json3-dev libyaml-dev libelf-dev \ libminizip-dev libcapstone-dev libgtk-4-dev \ @@ -124,7 +124,7 @@ jobs: export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install --yes --no-install-recommends \ - build-essential cmake git ninja-build pkg-config fakeroot \ + build-essential cmake git ninja-build pkg-config lld fakeroot \ dpkg-dev file libsdl3-dev libsdl3-ttf-dev \ libcurl4-openssl-dev libssl-dev nlohmann-json3-dev \ libyaml-dev libelf-dev libminizip-dev libcapstone-dev \ @@ -172,7 +172,7 @@ jobs: --env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \ fedora:44 bash -Eeuo pipefail -c ' dnf install --assumeyes \ - gcc-c++ cmake git ninja-build pkgconf-pkg-config rpm-build \ + gcc-c++ cmake git ninja-build pkgconf-pkg-config lld rpm-build \ SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \ nlohmann-json-devel libyaml-devel elfutils-libelf-devel \ minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \ @@ -218,7 +218,7 @@ jobs: --workdir /workspace \ archlinux:base-devel bash -Eeuo pipefail -c ' pacman -Syu --needed --noconfirm \ - cmake git ninja pkgconf sdl3 sdl3_ttf curl openssl \ + cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \ nlohmann-json libyaml libelf minizip capstone gtk4 \ libadwaita webkitgtk-6.0 libutf8proc fontconfig \ libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib diff --git a/CMakeLists.txt b/CMakeLists.txt index 75914a4..052c394 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -630,6 +630,40 @@ target_link_options(mocktail PRIVATE -Wl,--export-dynamic ) +# Native FreeBSD helper for Linuxulator's unsupported DF socket option. +option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER + "Build the native FreeBSD Linuxulator socket helper" ON) +if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER) +find_program(MOCKTAIL_LLD_EXECUTABLE NAMES ld.lld REQUIRED) +find_program(MOCKTAIL_ELFEDIT_EXECUTABLE NAMES elfedit REQUIRED) +get_filename_component(MOCKTAIL_LLD_DIRECTORY + "${MOCKTAIL_LLD_EXECUTABLE}" DIRECTORY) +set(MOCKTAIL_FREEBSD_SOCKET_HELPER + "${CMAKE_BINARY_DIR}/mocktail_freebsd_socket_helper") +add_custom_command( + OUTPUT "${MOCKTAIL_FREEBSD_SOCKET_HELPER}" + COMMAND "${CMAKE_CXX_COMPILER}" + -x assembler-with-cpp + -nostdlib + -static + -no-pie + "-B${MOCKTAIL_LLD_DIRECTORY}/" + -fuse-ld=lld + -Wl,-e,_start,--build-id=none,-z,noexecstack + -o "${MOCKTAIL_FREEBSD_SOCKET_HELPER}" + "${CMAKE_SOURCE_DIR}/src/compat/freebsd_socket_helper.S" + COMMAND "${MOCKTAIL_ELFEDIT_EXECUTABLE}" + --output-osabi FreeBSD "${MOCKTAIL_FREEBSD_SOCKET_HELPER}" + DEPENDS "${CMAKE_SOURCE_DIR}/src/compat/freebsd_socket_helper.S" + COMMENT "Building native FreeBSD Linuxulator socket helper" + VERBATIM +) +add_custom_target(mocktail_freebsd_socket_helper_build ALL + DEPENDS "${MOCKTAIL_FREEBSD_SOCKET_HELPER}" +) +add_dependencies(mocktail mocktail_freebsd_socket_helper_build) +endif() + set_target_properties(mocktail PROPERTIES BUILD_RPATH "\$ORIGIN" INSTALL_RPATH "\$ORIGIN;\$ORIGIN/../${CMAKE_INSTALL_LIBDIR}/mocktail" @@ -662,6 +696,11 @@ install(TARGETS RUNTIME DESTINATION "${MOCKTAIL_INSTALL_RUNTIME_DIR}" LIBRARY DESTINATION "${MOCKTAIL_INSTALL_RUNTIME_DIR}" ) +if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER) +install(PROGRAMS "${MOCKTAIL_FREEBSD_SOCKET_HELPER}" + DESTINATION "${MOCKTAIL_INSTALL_RUNTIME_DIR}" +) +endif() install(FILES config/roblox_compatibility.json config/roblox_bootstrap_sources.json diff --git a/README.md b/README.md index 77e633d..d6f4e18 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ [![Downloads](https://img.shields.io/github/downloads/komaruworld/mocktail/total?logo=github)](https://github.com/komaruworld/mocktail/releases/latest) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -Mocktail runs the Android `x86_64` Roblox client on Linux. It provides the -Android ABI and JNI pieces the client expects, then connects them to SDL3 and -Vulkan or OpenGL on the Linux side. +Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux +userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI +pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on +the Linux side. Mocktail is an independent community project. It is not affiliated with Roblox Corporation or VinegarHQ and does not distribute the Roblox client. @@ -102,17 +103,21 @@ Direct AppImage, DEB, and RPM downloads are available from the ## Building -Only Linux `x86_64` is supported. Building requires CMake 3.20+, Git, -pkg-config, a C++17 compiler, SDL 3.4+, SDL3_ttf, Vulkan, EGL, libplacebo, -fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip, Capstone 5, utf8proc, -nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0. +Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has +been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs +inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake +3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf, +Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip, +Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0. + +See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instructions.
Ubuntu 26.04+ ```bash sudo apt update -sudo apt install build-essential cmake git ninja-build pkg-config \ +sudo apt install build-essential cmake git ninja-build pkg-config lld \ libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev libssl-dev \ nlohmann-json3-dev libyaml-dev libelf-dev libminizip-dev \ libcapstone-dev libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev \ @@ -125,7 +130,7 @@ sudo apt install build-essential cmake git ninja-build pkg-config \ Arch Linux ```bash -sudo pacman -S --needed base-devel cmake git ninja pkgconf sdl3 sdl3_ttf \ +sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \ curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \ libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \ libplacebo vulkan-headers vulkan-icd-loader zlib @@ -136,7 +141,7 @@ sudo pacman -S --needed base-devel cmake git ninja pkgconf sdl3 sdl3_ttf \ Fedora 44+ ```bash -sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config \ +sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \ SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \ nlohmann-json-devel libyaml-devel elfutils-libelf-devel minizip-ng-compat-devel \ capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \ diff --git a/cmake/MocktailElfCompatibility.cmake b/cmake/MocktailElfCompatibility.cmake index ace4704..9c3621f 100644 --- a/cmake/MocktailElfCompatibility.cmake +++ b/cmake/MocktailElfCompatibility.cmake @@ -16,8 +16,10 @@ add_library(mocktail_compat STATIC ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_dns_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_host_libc_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_large_file_runtime.cc + ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_prctl_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_rwlock_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_signal_runtime.cc + ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_socket_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_semaphore_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_pthread_create_runtime.cc ${MOCKTAIL_ELF_COMPAT_ROOT}/src/compat/bionic_pthread_key_runtime.cc @@ -34,6 +36,9 @@ add_library(Mocktail::Compat ALIAS mocktail_compat) target_include_directories(mocktail_compat PUBLIC ${MOCKTAIL_ELF_COMPAT_ROOT}/include ) +target_compile_definitions(mocktail_compat PRIVATE + "MOCKTAIL_INSTALL_LIBDIR=\"${CMAKE_INSTALL_LIBDIR}\"" +) target_link_libraries(mocktail_compat PUBLIC PkgConfig::LIBELF nlohmann_json::nlohmann_json diff --git a/include/compat/bionic_host_libc_runtime.h b/include/compat/bionic_host_libc_runtime.h index 79b46a7..139777b 100644 --- a/include/compat/bionic_host_libc_runtime.h +++ b/include/compat/bionic_host_libc_runtime.h @@ -15,7 +15,10 @@ #ifndef MOCKTAIL_COMPAT_BIONIC_HOST_LIBC_RUNTIME_H_ #define MOCKTAIL_COMPAT_BIONIC_HOST_LIBC_RUNTIME_H_ +#include +#include #include +#include #include #include @@ -72,6 +75,15 @@ void BionicArc4RandomBuffer(void* buffer, size_t size) noexcept; BionicMallinfoSnapshot BionicMallinfo() noexcept; bool BionicMallinfoHasHostTelemetry() noexcept; +void NormalizeBionicSysInfo(struct sysinfo* info) noexcept; +int BionicSysInfo(struct sysinfo* info) noexcept; +bool NormalizeLinuxulatorUnameVersion(struct utsname* name, + const char* linux_version) noexcept; +int BionicUname(struct utsname* name) noexcept; +uid_t NormalizeBionicApplicationUid(uid_t host_uid) noexcept; +uid_t BionicGetUid() noexcept; +uid_t BionicGetEffectiveUid() noexcept; + // POSIX strerror_r contract: zero on success, -1/ERANGE on truncation. int BionicStrError(int error_number, char* buffer, size_t buffer_size) noexcept; @@ -93,6 +105,10 @@ int mocktail_bionic_cxa_thread_atexit_impl( void* dso_handle); void mocktail_bionic_arc4random_buf(void* buffer, size_t size); mocktail::compat::BionicMallinfoSnapshot mocktail_bionic_mallinfo(); +int mocktail_bionic_sysinfo(struct sysinfo* info); +int mocktail_bionic_uname(struct utsname* name); +uid_t mocktail_bionic_getuid(); +uid_t mocktail_bionic_geteuid(); int mocktail_bionic_strerror_r(int error_number, char* buffer, size_t buffer_size); long long mocktail_bionic_strtoll_l(const char* text, char** end, int base, diff --git a/include/compat/bionic_prctl_runtime.h b/include/compat/bionic_prctl_runtime.h new file mode 100644 index 0000000..999c9c6 --- /dev/null +++ b/include/compat/bionic_prctl_runtime.h @@ -0,0 +1,26 @@ +// Copyright 2026 Mocktail Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MOCKTAIL_COMPAT_BIONIC_PRCTL_RUNTIME_H_ +#define MOCKTAIL_COMPAT_BIONIC_PRCTL_RUNTIME_H_ + +extern "C" { + +int mocktail_bionic_prctl(int option, unsigned long argument2, + unsigned long argument3, unsigned long argument4, + unsigned long argument5); + +} // extern "C" + +#endif // MOCKTAIL_COMPAT_BIONIC_PRCTL_RUNTIME_H_ diff --git a/include/compat/bionic_socket_runtime.h b/include/compat/bionic_socket_runtime.h new file mode 100644 index 0000000..d98fc85 --- /dev/null +++ b/include/compat/bionic_socket_runtime.h @@ -0,0 +1,31 @@ +// Copyright 2026 Mocktail Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MOCKTAIL_COMPAT_BIONIC_SOCKET_RUNTIME_H_ +#define MOCKTAIL_COMPAT_BIONIC_SOCKET_RUNTIME_H_ + +#include + +extern "C" { + +int mocktail_bionic_setsockopt(int socket, int level, int option_name, + const void *option_value, + socklen_t option_length); + +ssize_t mocktail_bionic_sendmsg(int socket, const struct msghdr *message, + int flags); + +} // extern "C" + +#endif // MOCKTAIL_COMPAT_BIONIC_SOCKET_RUNTIME_H_ diff --git a/include/runtime/runtime_paths.h b/include/runtime/runtime_paths.h index 8b727df..7f7b68c 100644 --- a/include/runtime/runtime_paths.h +++ b/include/runtime/runtime_paths.h @@ -27,6 +27,8 @@ struct ActivePayloadPaths { bool active = false; std::filesystem::path root; std::filesystem::path roblox_library; + std::filesystem::path base_apk; + std::filesystem::path x86_64_split_apk; std::filesystem::path assets_content; std::filesystem::path compatibility_manifest; std::filesystem::path host_abi_profile; @@ -127,9 +129,7 @@ class RuntimePaths { bool ExportRuntimePathEnvironment(const RuntimePaths& paths, std::string* error = nullptr); -// Selects the writable XDG data root as the managed payload working directory -// and exposes its immutable assets at the relative path still used by a small -// number of guest filesystem probes. +// Publishes the managed payload under the guest's rbx_bin paths. bool PrepareManagedPayloadWorkingDirectory(const RuntimePaths& paths, const ActivePayloadPaths& active, std::string* error = nullptr); diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 3277623..28f14a2 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -30,6 +30,7 @@ depends=( makedepends=( 'cmake>=3.20' 'git' + 'lld' 'ninja' 'pkgconf' ) diff --git a/packaging/aur/mocktail-git/.SRCINFO b/packaging/aur/mocktail-git/.SRCINFO index dffb998..effcd15 100644 --- a/packaging/aur/mocktail-git/.SRCINFO +++ b/packaging/aur/mocktail-git/.SRCINFO @@ -7,6 +7,7 @@ pkgbase = mocktail-git license = Apache-2.0 makedepends = cmake>=3.20 makedepends = git + makedepends = lld makedepends = ninja makedepends = nlohmann-json makedepends = pkgconf diff --git a/packaging/aur/mocktail-git/PKGBUILD b/packaging/aur/mocktail-git/PKGBUILD index 7241600..99a101b 100644 --- a/packaging/aur/mocktail-git/PKGBUILD +++ b/packaging/aur/mocktail-git/PKGBUILD @@ -34,6 +34,7 @@ depends=( makedepends=( 'cmake>=3.20' 'git' + 'lld' 'ninja' 'nlohmann-json' 'pkgconf' diff --git a/packaging/aur/mocktail/.SRCINFO b/packaging/aur/mocktail/.SRCINFO index 378be2d..a33d97d 100644 --- a/packaging/aur/mocktail/.SRCINFO +++ b/packaging/aur/mocktail/.SRCINFO @@ -7,6 +7,7 @@ pkgbase = mocktail license = Apache-2.0 makedepends = cmake>=3.20 makedepends = git + makedepends = lld makedepends = ninja makedepends = nlohmann-json makedepends = pkgconf diff --git a/packaging/aur/mocktail/PKGBUILD b/packaging/aur/mocktail/PKGBUILD index c8da329..49d8aa9 100644 --- a/packaging/aur/mocktail/PKGBUILD +++ b/packaging/aur/mocktail/PKGBUILD @@ -34,6 +34,7 @@ depends=( makedepends=( 'cmake>=3.20' 'git' + 'lld' 'ninja' 'nlohmann-json' 'pkgconf' diff --git a/packaging/flatpak/space.bigrat.mocktail.json b/packaging/flatpak/space.bigrat.mocktail.json index f44a625..41f1497 100644 --- a/packaging/flatpak/space.bigrat.mocktail.json +++ b/packaging/flatpak/space.bigrat.mocktail.json @@ -179,6 +179,7 @@ "-DCMAKE_INSTALL_LIBDIR=lib", "-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=/app/share/mocktail/metadata/roblox_compatibility.json", "-DMOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=/app/share/mocktail/metadata/roblox_signing_certificates.json", + "-DMOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER=OFF", "-DBUILD_TESTING=OFF" ], "sources": [ diff --git a/packaging/freebsd/README.md b/packaging/freebsd/README.md new file mode 100644 index 0000000..f610973 --- /dev/null +++ b/packaging/freebsd/README.md @@ -0,0 +1,51 @@ +# FreeBSD Linuxulator + +FreeBSD Linuxulator support is experimental. I tested it on FreeBSD +15.1-RELEASE-p2 with a Fedora 44 x86_64 userspace. + +FreeBSD returns the wrong filesystem type for `/proc`. Roblox treats this as a +broken install and disconnects with Error 304 after about a minute. +[`linuxulator.patch`](linuxulator.patch) makes Linuxulator return the same +value as Linux. + +## Patch FreeBSD + +Use a clean Git checkout that exactly matches the running FreeBSD kernel. The +tested source revision is `aadd58dddcbc78f4d5594827b46b5633552b15ce`. Run +the following commands as root. + +```sh +fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/komaruworld/mocktail/main/packaging/freebsd/linuxulator.patch +cd /usr/src +git apply /root/linuxulator.patch +make -j2 kernel-toolchain +make -j2 buildkernel KERNCONF=GENERIC MODULES_OVERRIDE=linux64 +``` + +Install only the rebuilt module. Keep a backup and reboot instead of unloading +a live Linuxulator module. + +```sh +cp -p /boot/kernel/linux64.ko /boot/kernel/linux64.ko.mocktail-backup +install -o root -g wheel -m 0444 /usr/obj/usr/src/amd64.amd64/sys/GENERIC/modules/usr/src/sys/modules/linux64/linux64.ko /boot/kernel/linux64.ko.mocktail-new +mv /boot/kernel/linux64.ko.mocktail-new /boot/kernel/linux64.ko +kldxref /boot/kernel +reboot +``` + +After reboot, verify the result from the Fedora userspace. It must print +`9fa0`. + +```sh +chroot /compat/linux stat -f -c %t /proc +``` + +## Run Mocktail + +You can simply run the Mocktail AppImage from the +[latest release](https://github.com/komaruworld/mocktail/releases/latest) or +[nightly build](https://github.com/komaruworld/mocktail/releases/tag/continuous) +inside the Fedora userspace. + +Mocktail cannot override this check because Roblox reads `/proc` directly. The +Linuxulator patch fixes the value before Roblox sees it. diff --git a/packaging/freebsd/linuxulator.patch b/packaging/freebsd/linuxulator.patch new file mode 100644 index 0000000..30d84ec --- /dev/null +++ b/packaging/freebsd/linuxulator.patch @@ -0,0 +1,11 @@ +diff --git a/sys/compat/linux/linux_stats.c b/sys/compat/linux/linux_stats.c +--- a/sys/compat/linux/linux_stats.c ++++ b/sys/compat/linux/linux_stats.c +@@ -308,6 +308,7 @@ bsd_to_linux_ftype(const char *fstypename) + {"nfs", LINUX_NFS_SUPER_MAGIC}, + {"ext2fs", LINUX_EXT2_SUPER_MAGIC}, + {"procfs", LINUX_PROC_SUPER_MAGIC}, ++ {"linprocfs", LINUX_PROC_SUPER_MAGIC}, + {"msdosfs", LINUX_MSDOS_SUPER_MAGIC}, + {"ntfs", LINUX_NTFS_SUPER_MAGIC}, + {"nwfs", LINUX_NCP_SUPER_MAGIC}, diff --git a/packaging/mocktail-launcher.sh b/packaging/mocktail-launcher.sh index 529c6fb..abeb09b 100755 --- a/packaging/mocktail-launcher.sh +++ b/packaging/mocktail-launcher.sh @@ -29,8 +29,10 @@ fi RUNTIME_ROOT="${BUNDLE_ROOT}/mocktail" BIN_DIR="${RUNTIME_ROOT}/bin" MAIN_BINARY="${BIN_DIR}/mocktail" +UPDATE_HELPER="${BIN_DIR}/mocktail_updater" FAILURE_DIALOG_HELPER="${BIN_DIR}/mocktail_failure_dialog" WEBVIEW_HELPER="${BIN_DIR}/mocktail_webview_helper" +FREEBSD_SOCKET_HELPER="${BIN_DIR}/mocktail_freebsd_socket_helper" ANDROID_BUILD_TOOLS="${RUNTIME_ROOT}/runtime/android-tools/bin" METADATA_DIR="${RUNTIME_ROOT}/metadata" ABI_MANIFEST="${METADATA_DIR}/ABI.txt" @@ -40,6 +42,7 @@ SUPPORT_ROOT="${RUNTIME_ROOT}/runtime" SUPPORT_BIN="${SUPPORT_ROOT}/bin" export PATH="${SUPPORT_BIN}:${ANDROID_BUILD_TOOLS}:${PATH}" export LD_LIBRARY_PATH="${RUNTIME_ROOT}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +export MOCKTAIL_FREEBSD_SOCKET_HELPER="${FREEBSD_SOCKET_HELPER}" Die() { printf 'mocktail: %s\n' "$*" >&2 @@ -346,6 +349,7 @@ CheckBundleAbi() { [[ "${main_interpreter}" == "${ABI_INTERPRETER}" ]] || Die "mocktail ELF interpreter does not match ABI.txt (expected ${ABI_INTERPRETER}, found ${main_interpreter:-none})" ValidateBundledElfAbi "${MAIN_BINARY}" "mocktail" + ValidateBundledElfAbi "${UPDATE_HELPER}" "Mocktail updater" ValidateBundledElfAbi "${WEBVIEW_HELPER}" "WebView helper" [[ -x "${ANDROID_BUILD_TOOLS}/aapt" ]] || Die "Android APK metadata tool is unavailable" @@ -475,6 +479,7 @@ CheckSystem() { [[ -d "${RUNTIME_ROOT}" && ! -L "${RUNTIME_ROOT}" ]] || Die "missing regular runtime directory: ${RUNTIME_ROOT}" [[ -x "${MAIN_BINARY}" ]] || Die "missing executable: ${MAIN_BINARY}" +[[ -x "${UPDATE_HELPER}" ]] || Die "missing executable: ${UPDATE_HELPER}" [[ -x "${FAILURE_DIALOG_HELPER}" ]] || Die "missing executable: ${FAILURE_DIALOG_HELPER}" [[ -x "${WEBVIEW_HELPER}" ]] || Die "missing WebView helper: ${WEBVIEW_HELPER}" @@ -513,6 +518,7 @@ export MOCKTAIL_UPDATE_COMPATIBILITY_PATH="${METADATA_DIR}/roblox_compatibility. export MOCKTAIL_UPDATE_SIGNING_TRUST_PATH="${METADATA_DIR}/roblox_signing_certificates.json" export MOCKTAIL_UPDATE_HOST_ABI_REFERENCE="${METADATA_DIR}/roblox_host_abi_reference.json" export MOCKTAIL_BOOTSTRAP_SOURCES_PATH="${METADATA_DIR}/roblox_bootstrap_sources.json" +export MOCKTAIL_UPDATE_HELPER="${UPDATE_HELPER}" export MOCKTAIL_UPDATE_CANARY_BIN="${MAIN_BINARY}" export MOCKTAIL_UPDATE_SMOKE_SCRIPT="${RUNTIME_ROOT}/scripts/real_bringup_smoke.sh" export MOCKTAIL_BIN="${MAIN_BINARY}" diff --git a/scripts/build.sh b/scripts/build.sh index 495b501..49be5aa 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -158,6 +158,8 @@ if [[ -z "${CMAKE_TOOLCHAIN_FILE}" ]]; then fi check_tool git "sudo apt install git" check_tool java "sudo apt install default-jdk" +check_tool ld.lld "install LLD" +check_tool elfedit "install binutils" if [[ "${EXTRACT_APK}" == true ]]; then check_tool unzip "sudo apt install unzip" diff --git a/scripts/build_release.sh b/scripts/build_release.sh index 16ee872..cf379b9 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -277,6 +277,8 @@ Main() { ParseArguments "$@" RequireCommand cmake RequireCommand find + RequireCommand ld.lld + RequireCommand elfedit RequireCommand readelf RequireCommand readlink RequireCommand sort diff --git a/scripts/package_portable.sh b/scripts/package_portable.sh index 165784a..e3e0780 100755 --- a/scripts/package_portable.sh +++ b/scripts/package_portable.sh @@ -31,6 +31,7 @@ ANDROID_TOOLS_ABI="" readonly -a PROJECT_ARTIFACTS=( mocktail + mocktail_updater mocktail_failure_dialog mocktail_webview_helper libmocktail_audio_sdl.so @@ -47,6 +48,7 @@ readonly -a PROJECT_ARTIFACTS=( libGLESv2.so libvulkan.so ) +readonly FREEBSD_SOCKET_HELPER="mocktail_freebsd_socket_helper" readonly -a RUNTIME_SCRIPTS=( auto_update_roblox.sh @@ -267,6 +269,21 @@ ValidateArtifacts() { TARGET_INTERPRETER="$(ReadElfInterpreter "${BUILD_DIR}/mocktail")" [[ -n "${TARGET_INTERPRETER}" ]] || Die "mocktail must be a dynamically linked ${TARGET_LIBC} executable" + + local freebsd_helper="${BUILD_DIR}/${FREEBSD_SOCKET_HELPER}" + [[ -f "${freebsd_helper}" && ! -L "${freebsd_helper}" && + -x "${freebsd_helper}" ]] || + Die "missing executable FreeBSD socket helper: ${freebsd_helper}" + LC_ALL=C readelf -h "${freebsd_helper}" 2>/dev/null | + grep -Eq 'OS/ABI:[[:space:]]+UNIX - FreeBSD' || + Die "socket helper is not branded for FreeBSD" + LC_ALL=C readelf -h "${freebsd_helper}" 2>/dev/null | + grep -Eq 'Machine:[[:space:]]+.*X86-64' || + Die "socket helper is not an x86-64 ELF" + [[ -z "$(ReadElfInterpreter "${freebsd_helper}")" ]] || + Die "FreeBSD socket helper must be statically linked" + LC_ALL=C readelf -n "${freebsd_helper}" 2>/dev/null | + grep -Fq FreeBSD || Die "socket helper has no FreeBSD ABI note" } ResolveAndroidBuildTools() { @@ -468,6 +485,8 @@ CopyRuntimeTree() { install -m "${mode}" -- "${BUILD_DIR}/${artifact}" \ "${runtime_root}/bin/${artifact}" done + install -m 0755 -- "${BUILD_DIR}/${FREEBSD_SOCKET_HELPER}" \ + "${runtime_root}/bin/${FREEBSD_SOCKET_HELPER}" install -m 0755 -- "${PROJECT_ROOT}/packaging/run.sh" "${STAGING}/run.sh" install -m 0755 -- "${PROJECT_ROOT}/packaging/mocktail-launcher.sh" \ @@ -625,6 +644,7 @@ QueuePackagedRuntimeDependencies() { while IFS= read -r -d '' source; do IsElfFile "${source}" || continue case "${source#${STAGING}/mocktail/}" in + bin/${FREEBSD_SOCKET_HELPER}) continue ;; runtime/android-tools/*) continue ;; esac QueueDependencies "${source}" diff --git a/src/compat/bionic_host_libc_runtime.cc b/src/compat/bionic_host_libc_runtime.cc index ce92cee..4286144 100644 --- a/src/compat/bionic_host_libc_runtime.cc +++ b/src/compat/bionic_host_libc_runtime.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,35 @@ bool FillFromGetRandom(unsigned char* output, size_t size) noexcept { return true; } +bool ReadTextFile(const char* path, char* output, size_t capacity) noexcept { + if (path == nullptr || output == nullptr || capacity < 2) return false; + + int descriptor; + do { + descriptor = ::open(path, O_RDONLY | O_CLOEXEC); + } while (descriptor < 0 && errno == EINTR); + if (descriptor < 0) return false; + + ssize_t length; + do { + length = ::read(descriptor, output, capacity - 1); + } while (length < 0 && errno == EINTR); + const int read_errno = errno; + static_cast(::close(descriptor)); + if (length <= 0) { + errno = read_errno; + return false; + } + + size_t size = static_cast(length); + while (size != 0 && + (output[size - 1] == '\n' || output[size - 1] == '\r')) { + --size; + } + output[size] = '\0'; + return size != 0; +} + void FillFromUrandom(unsigned char* output, size_t size) noexcept { int descriptor; do { @@ -238,6 +268,62 @@ bool BionicMallinfoHasHostTelemetry() noexcept { return false; } +void NormalizeBionicSysInfo(struct sysinfo* info) noexcept { + if (info == nullptr) return; + constexpr unsigned long kHighBit = + 1UL << (sizeof(unsigned long) * CHAR_BIT - 1U); + if ((info->totalswap & kHighBit) != 0 || + (info->freeswap & kHighBit) != 0 || info->freeswap > info->totalswap) { + info->totalswap = 0; + info->freeswap = 0; + } +} + +int BionicSysInfo(struct sysinfo* info) noexcept { + const int result = ::sysinfo(info); + if (result == 0) NormalizeBionicSysInfo(info); + return result; +} + +bool NormalizeLinuxulatorUnameVersion(struct utsname* name, + const char* linux_version) noexcept { + if (name == nullptr || linux_version == nullptr || linux_version[0] == '\0' || + std::strcmp(name->sysname, "Linux") != 0 || + (std::strstr(name->version, "FreeBSD") == nullptr && + std::strstr(name->version, "freebsd.org") == nullptr)) { + return false; + } + const int length = std::snprintf(name->version, sizeof(name->version), "%s", + linux_version); + return length >= 0 && static_cast(length) < sizeof(name->version); +} + +int BionicUname(struct utsname* name) noexcept { + const int result = ::uname(name); + if (result != 0 || name == nullptr) return result; + + const int host_errno = errno; + std::arrayversion)> linux_version{}; + if (ReadTextFile("/proc/sys/kernel/version", linux_version.data(), + linux_version.size())) { + NormalizeLinuxulatorUnameVersion(name, linux_version.data()); + } + errno = host_errno; + return result; +} + +uid_t NormalizeBionicApplicationUid(uid_t host_uid) noexcept { + return host_uid == 0 ? 1000 : host_uid; +} + +uid_t BionicGetUid() noexcept { + return NormalizeBionicApplicationUid(::getuid()); +} + +uid_t BionicGetEffectiveUid() noexcept { + return NormalizeBionicApplicationUid(::geteuid()); +} + int BionicStrError(int error_number, char* buffer, size_t buffer_size) noexcept { const int entry_errno = errno; @@ -305,6 +391,22 @@ extern "C" mocktail::compat::BionicMallinfoSnapshot mocktail_bionic_mallinfo() { return mocktail::compat::BionicMallinfo(); } +extern "C" int mocktail_bionic_sysinfo(struct sysinfo* info) { + return mocktail::compat::BionicSysInfo(info); +} + +extern "C" int mocktail_bionic_uname(struct utsname* name) { + return mocktail::compat::BionicUname(name); +} + +extern "C" uid_t mocktail_bionic_getuid() { + return mocktail::compat::BionicGetUid(); +} + +extern "C" uid_t mocktail_bionic_geteuid() { + return mocktail::compat::BionicGetEffectiveUid(); +} + extern "C" int mocktail_bionic_strerror_r(int error_number, char* buffer, size_t buffer_size) { return mocktail::compat::BionicStrError(error_number, buffer, buffer_size); diff --git a/src/compat/bionic_prctl_runtime.cc b/src/compat/bionic_prctl_runtime.cc new file mode 100644 index 0000000..0e879d3 --- /dev/null +++ b/src/compat/bionic_prctl_runtime.cc @@ -0,0 +1,119 @@ +// Copyright 2026 Mocktail Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "compat/bionic_prctl_runtime.h" + +#include +#include +#include +#include +#include + +namespace mocktail::compat { +namespace { + +#ifndef PR_SET_VMA +constexpr int kPrSetVma = 0x53564d41; +#else +constexpr int kPrSetVma = PR_SET_VMA; +#endif + +#ifndef PR_SET_VMA_ANON_NAME +constexpr unsigned long kPrSetVmaAnonName = 0; +#else +constexpr unsigned long kPrSetVmaAnonName = PR_SET_VMA_ANON_NAME; +#endif + +#ifndef PR_SET_THP_DISABLE +constexpr int kPrSetThpDisable = 41; +#else +constexpr int kPrSetThpDisable = PR_SET_THP_DISABLE; +#endif + +#ifndef PR_GET_THP_DISABLE +constexpr int kPrGetThpDisable = 42; +#else +constexpr int kPrGetThpDisable = PR_GET_THP_DISABLE; +#endif + +bool IsUnsupportedPrctlError(int error) noexcept { + return error == EINVAL || error == ENOSYS || error == EOPNOTSUPP; +} + +bool IsFreeBsdLinuxulator() noexcept { + const int saved_errno = errno; + static const bool detected = [] { + int descriptor = ::open("/proc/version", O_RDONLY | O_CLOEXEC); + if (descriptor < 0) return false; + + char version[512] = {}; + const ssize_t length = ::read(descriptor, version, sizeof(version) - 1); + ::close(descriptor); + if (length <= 0) return false; + version[length] = '\0'; + return std::strstr(version, "FreeBSD") != nullptr || + std::strstr(version, "freebsd.org") != nullptr; + }(); + errno = saved_errno; + return detected; +} + +bool IsPageAlignedRange(unsigned long address, unsigned long length) noexcept { + if (address == 0 || length == 0 || address + length < address) return false; + const long page_size = ::sysconf(_SC_PAGESIZE); + if (page_size <= 0) return false; + const auto page_mask = static_cast(page_size - 1); + return (address & page_mask) == 0 && (length & page_mask) == 0; +} + +} // namespace + +int BionicPrctl(int option, unsigned long argument2, + unsigned long argument3, unsigned long argument4, + unsigned long argument5) noexcept { + const int previous_errno = errno; + const int result = ::prctl(option, argument2, argument3, argument4, + argument5); + if (result != -1 || !IsFreeBsdLinuxulator() || + !IsUnsupportedPrctlError(errno)) { + return result; + } + + if (option == kPrGetThpDisable) { + errno = previous_errno; + return 1; + } + if (option == kPrSetThpDisable && argument2 != 0) { + errno = previous_errno; + return 0; + } + + if (option == kPrSetVma && argument2 == kPrSetVmaAnonName && + IsPageAlignedRange(argument3, argument4)) { + errno = previous_errno; + return 0; + } + + return result; +} + +} // namespace mocktail::compat + +extern "C" int mocktail_bionic_prctl(int option, unsigned long argument2, + unsigned long argument3, + unsigned long argument4, + unsigned long argument5) { + return mocktail::compat::BionicPrctl(option, argument2, argument3, + argument4, argument5); +} diff --git a/src/compat/bionic_socket_runtime.cc b/src/compat/bionic_socket_runtime.cc new file mode 100644 index 0000000..89918ed --- /dev/null +++ b/src/compat/bionic_socket_runtime.cc @@ -0,0 +1,395 @@ +// Copyright 2026 Mocktail Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "compat/bionic_socket_runtime.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +#ifndef MOCKTAIL_INSTALL_LIBDIR +#define MOCKTAIL_INSTALL_LIBDIR "lib" +#endif + +namespace mocktail::compat { +namespace { + +constexpr const char kFreeBsdSocketHelper[] = + "mocktail_freebsd_socket_helper"; + +#ifndef SOL_UDP +constexpr int kSolUdp = 17; +#else +constexpr int kSolUdp = SOL_UDP; +#endif + +#ifndef UDP_SEGMENT +constexpr int kUdpSegment = 103; +#else +constexpr int kUdpSegment = UDP_SEGMENT; +#endif + +#ifndef UDP_GRO +constexpr int kUdpGro = 104; +#else +constexpr int kUdpGro = UDP_GRO; +#endif + +bool IsFreeBsdLinuxulator() noexcept { + const int saved_errno = errno; + static const bool detected = [] { + int descriptor = ::open("/proc/version", O_RDONLY | O_CLOEXEC); + if (descriptor < 0) return false; + char version[512] = {}; + const ssize_t length = ::read(descriptor, version, sizeof(version) - 1); + ::close(descriptor); + if (length <= 0) return false; + version[length] = '\0'; + return std::strstr(version, "FreeBSD") != nullptr || + std::strstr(version, "freebsd.org") != nullptr; + }(); + errno = saved_errno; + return detected; +} + +bool IsUnsupportedSocketOptionError(int error) noexcept { + return error == ENOPROTOOPT || error == EINVAL || error == EOPNOTSUPP; +} + +struct UdpSegmentControl { + bool present = false; + uint16_t segment_size = 0; +}; + +struct ParsedControlMessages { + UdpSegmentControl udp_segment; + std::vector passthrough; +}; + +bool ParseControlMessages(const msghdr *message, + ParsedControlMessages *result) { + if (message == nullptr || result == nullptr) return false; + if (message->msg_controllen == 0) return true; + if (message->msg_control == nullptr) return false; + + const auto *cursor = + static_cast(message->msg_control); + std::size_t remaining = message->msg_controllen; + bool udp_segment_seen = false; + while (remaining >= sizeof(cmsghdr)) { + cmsghdr header{}; + std::memcpy(&header, cursor, sizeof(header)); + const std::size_t length = header.cmsg_len; + if (length < sizeof(cmsghdr) || length > remaining || + length > SIZE_MAX - (sizeof(size_t) - 1U)) { + return false; + } + const std::size_t aligned_length = CMSG_ALIGN(length); + if (aligned_length < length) return false; + + const bool udp_segment = + header.cmsg_level == kSolUdp && header.cmsg_type == kUdpSegment; + if (udp_segment) { + if (length < CMSG_LEN(sizeof(uint16_t))) return false; + if (!udp_segment_seen) { + std::memcpy(&result->udp_segment.segment_size, + cursor + CMSG_ALIGN(sizeof(cmsghdr)), sizeof(uint16_t)); + result->udp_segment.present = + result->udp_segment.segment_size != 0; + udp_segment_seen = true; + } + } else { + const std::size_t old_size = result->passthrough.size(); + if (aligned_length > SIZE_MAX - old_size || + aligned_length > result->passthrough.max_size() - old_size) { + return false; + } + result->passthrough.resize(old_size + aligned_length, 0); + std::memcpy(result->passthrough.data() + old_size, cursor, length); + } + + const std::size_t consumed = std::min(aligned_length, remaining); + cursor += consumed; + remaining -= consumed; + } + return true; +} + +bool IovecByteCount(const msghdr *message, std::size_t *result) noexcept { + if (message == nullptr || result == nullptr || + message->msg_iovlen > static_cast(IOV_MAX) || + (message->msg_iovlen != 0 && message->msg_iov == nullptr)) { + return false; + } + std::size_t total = 0; + for (std::size_t index = 0; index < message->msg_iovlen; ++index) { + const std::size_t length = message->msg_iov[index].iov_len; + if (length > SIZE_MAX - total) return false; + total += length; + } + *result = total; + return true; +} + +std::vector SliceIovecs(const msghdr *message, std::size_t offset, + std::size_t length) { + std::vector result; + for (std::size_t index = 0; + index < message->msg_iovlen && length != 0; ++index) { + const iovec &source = message->msg_iov[index]; + if (offset >= source.iov_len) { + offset -= source.iov_len; + continue; + } + const std::size_t available = source.iov_len - offset; + const std::size_t selected = available < length ? available : length; + auto *base = static_cast(source.iov_base) + offset; + result.push_back({base, selected}); + length -= selected; + offset = 0; + } + return result; +} + +bool DiscoveryModeUsesDontFragment(int mode, bool *enabled) noexcept { + if (enabled == nullptr) return false; + switch (mode) { + case IP_PMTUDISC_DONT: + *enabled = false; + return true; + case IP_PMTUDISC_WANT: + case IP_PMTUDISC_DO: + case IP_PMTUDISC_PROBE: + *enabled = true; + return true; +#ifdef IP_PMTUDISC_INTERFACE + case IP_PMTUDISC_INTERFACE: + *enabled = false; + return true; +#endif +#ifdef IP_PMTUDISC_OMIT + case IP_PMTUDISC_OMIT: + *enabled = false; + return true; +#endif + default: + return false; + } +} + +std::string ResolveFreeBsdSocketHelperPath(const std::string &executable_path, + const char *override_path) { + if (override_path != nullptr && override_path[0] == '/') { + return override_path; + } + + const std::size_t separator = executable_path.find_last_of('/'); + if (separator == std::string::npos) return {}; + const std::string executable_directory = + executable_path.substr(0, separator + 1); + const std::string adjacent = executable_directory + kFreeBsdSocketHelper; + if (::access(adjacent.c_str(), X_OK) == 0) return adjacent; + + constexpr const char kInstallLibDir[] = MOCKTAIL_INSTALL_LIBDIR; + const std::string installed = + kInstallLibDir[0] == '/' + ? std::string(kInstallLibDir) + "/mocktail/" + + kFreeBsdSocketHelper + : executable_directory + "../" + kInstallLibDir + "/mocktail/" + + kFreeBsdSocketHelper; + if (::access(installed.c_str(), X_OK) == 0) return installed; + return {}; +} + +std::string FreeBsdSocketHelperPath() { + std::array executable{}; + const ssize_t length = + ::readlink("/proc/self/exe", executable.data(), PATH_MAX); + if (length <= 0 || length > PATH_MAX) return {}; + executable[static_cast(length)] = '\0'; + return ResolveFreeBsdSocketHelperPath( + std::string(executable.data(), static_cast(length)), + std::getenv("MOCKTAIL_FREEBSD_SOCKET_HELPER")); +} + +int ApplyFreeBsdPathMtuDiscovery(int socket, int discovery_mode) noexcept { + bool dont_fragment = false; + if (!DiscoveryModeUsesDontFragment(discovery_mode, &dont_fragment)) { + return -1; + } + + const std::string helper = FreeBsdSocketHelperPath(); + if (helper.empty() || ::access(helper.c_str(), X_OK) != 0) return -1; + + const int inherited_socket = ::fcntl(socket, F_DUPFD, 3); + if (inherited_socket < 0) return -1; + + std::array descriptor{}; + const int descriptor_length = std::snprintf( + descriptor.data(), descriptor.size(), "%d", inherited_socket); + if (descriptor_length <= 0 || + static_cast(descriptor_length) >= descriptor.size()) { + ::close(inherited_socket); + return -1; + } + + char enabled[] = "0"; + if (dont_fragment) enabled[0] = '1'; + char *arguments[] = { + const_cast(kFreeBsdSocketHelper), descriptor.data(), enabled, + nullptr}; + pid_t child = -1; + const int spawn_status = + ::posix_spawn(&child, helper.c_str(), nullptr, nullptr, arguments, + environ); + ::close(inherited_socket); + if (spawn_status != 0) return -1; + + int child_status = 0; + pid_t waited = -1; + do { + waited = ::waitpid(child, &child_status, 0); + } while (waited < 0 && errno == EINTR); + if (waited != child || !WIFEXITED(child_status) || + WEXITSTATUS(child_status) != 0) { + return -1; + } + + return 0; +} + +} // namespace + +int BionicSetsockopt(int socket, int level, int option_name, + const void *option_value, + socklen_t option_length) noexcept { + const int previous_errno = errno; + const int result = + ::setsockopt(socket, level, option_name, option_value, option_length); + if (result == -1 && errno == ENOPROTOOPT && level == IPPROTO_IP && + option_name == IP_MTU_DISCOVER && option_value != nullptr && + option_length == sizeof(int)) { + const int discovery_mode = *static_cast(option_value); + if (ApplyFreeBsdPathMtuDiscovery(socket, discovery_mode) == 0) { + errno = previous_errno; + return 0; + } + bool dont_fragment = false; + if (IsFreeBsdLinuxulator() && + DiscoveryModeUsesDontFragment(discovery_mode, &dont_fragment)) { + errno = previous_errno; + return 0; + } + errno = ENOPROTOOPT; + } + if (result == -1 && IsFreeBsdLinuxulator() && + IsUnsupportedSocketOptionError(errno) && level == kSolUdp && + option_name == kUdpGro && option_value != nullptr && + option_length == sizeof(int)) { + const int enabled = *static_cast(option_value); + if (enabled == 0 || enabled == 1) { + errno = previous_errno; + return 0; + } + } + return result; +} + +ssize_t BionicSendmsg(int socket, const msghdr *message, int flags) noexcept { + if (message == nullptr) { + errno = EINVAL; + return -1; + } + + const int previous_errno = errno; + const ssize_t result = ::sendmsg(socket, message, flags); + if (result != -1 || !IsFreeBsdLinuxulator() || + !IsUnsupportedSocketOptionError(errno)) { + return result; + } + + ParsedControlMessages control; + if (!ParseControlMessages(message, &control)) { + errno = EINVAL; + return -1; + } + if (!control.udp_segment.present) return result; + std::size_t total = 0; + if (!IovecByteCount(message, &total)) { + errno = EINVAL; + return -1; + } + if (total == 0) return result; + + std::size_t sent_total = 0; + while (sent_total < total) { + const std::size_t segment_length = + std::min(control.udp_segment.segment_size, + total - sent_total); + std::vector iovecs = + SliceIovecs(message, sent_total, segment_length); + if (iovecs.empty()) { + errno = EINVAL; + return -1; + } + + msghdr segment = *message; + segment.msg_iov = iovecs.data(); + segment.msg_iovlen = iovecs.size(); + segment.msg_control = + control.passthrough.empty() ? nullptr : control.passthrough.data(); + segment.msg_controllen = control.passthrough.size(); + const ssize_t sent = ::sendmsg(socket, &segment, flags); + if (sent < 0) return -1; + if (static_cast(sent) != segment_length) { + errno = EIO; + return -1; + } + sent_total += segment_length; + } + + errno = previous_errno; + return static_cast(sent_total); +} + +} // namespace mocktail::compat + +extern "C" int mocktail_bionic_setsockopt(int socket, int level, + int option_name, + const void *option_value, + socklen_t option_length) { + return mocktail::compat::BionicSetsockopt(socket, level, option_name, + option_value, option_length); +} + +extern "C" ssize_t mocktail_bionic_sendmsg(int socket, + const struct msghdr *message, + int flags) { + return mocktail::compat::BionicSendmsg(socket, message, flags); +} diff --git a/src/compat/bionic_sysconf.cc b/src/compat/bionic_sysconf.cc index 41b6db8..3b3b95a 100644 --- a/src/compat/bionic_sysconf.cc +++ b/src/compat/bionic_sysconf.cc @@ -26,7 +26,8 @@ long BionicSysconf(int name) noexcept { case BionicSysconfName::kChildMax: return ::sysconf(_SC_CHILD_MAX); case BionicSysconfName::kClockTicks: - return ::sysconf(_SC_CLK_TCK); + // Android/Linux USER_HZ is 100. + return 100; case BionicSysconfName::kOpenMax: return ::sysconf(_SC_OPEN_MAX); case BionicSysconfName::kIovMax: @@ -48,7 +49,7 @@ long BionicSysconf(int name) noexcept { return -1; } -} // namespace mocktail::compat +} // namespace mocktail::compat extern "C" long mocktail_bionic_sysconf(int name) { return mocktail::compat::BionicSysconf(name); diff --git a/src/compat/freebsd_socket_helper.S b/src/compat/freebsd_socket_helper.S new file mode 100644 index 0000000..5407c86 --- /dev/null +++ b/src/compat/freebsd_socket_helper.S @@ -0,0 +1,103 @@ +/* + * Copyright 2026 Mocktail Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* argv: inherited socket descriptor, IP_DONTFRAG value. */ + + .section .note.tag,"a",@note + .p2align 2 + .long 8 + .long 4 + .long 1 + .asciz "FreeBSD" + .p2align 2 + .long 1500000 + + .text + .globl _start + .type _start,@function +_start: + cmpq $3, (%rsp) + jne .Lusage + + movq 16(%rsp), %rsi + xorl %edi, %edi +.Lparse_fd: + movzbl (%rsi), %eax + testb %al, %al + je .Lfd_ready + subl $'0', %eax + cmpl $9, %eax + ja .Lusage + imull $10, %edi, %edi + addl %eax, %edi + incq %rsi + jmp .Lparse_fd + +.Lfd_ready: + movq 24(%rsp), %rsi + cmpb $0, 1(%rsi) + jne .Lusage + movzbl (%rsi), %eax + cmpb $'0', %al + je .Ldisable + cmpb $'1', %al + jne .Lusage + movl $1, %eax + jmp .Lvalue_ready +.Ldisable: + xorl %eax, %eax + +.Lvalue_ready: + subq $8, %rsp + movl %eax, (%rsp) + + movl $105, %eax + xorl %esi, %esi + movl $67, %edx + movq %rsp, %r10 + movl $4, %r8d + syscall + jnc .Lsuccess + + /* Retry with IPV6_DONTFRAG for dual-stack sockets. */ + movl $105, %eax + movl $41, %esi + movl $62, %edx + movq %rsp, %r10 + movl $4, %r8d + syscall + jc .Lerror + +.Lsuccess: + xorl %edi, %edi + jmp .Lexit +.Lerror: + movl %eax, %edi + andl $255, %edi + testl %edi, %edi + jne .Lexit + movl $1, %edi + jmp .Lexit +.Lusage: + movl $64, %edi + +.Lexit: + movl $1, %eax + syscall + ud2 + .size _start, .-_start + + .section .note.GNU-stack,"",@progbits diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index d188976..40d7b66 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -52,7 +52,9 @@ #include #include "compat/bionic_abi_exports.h" +#include "compat/bionic_prctl_runtime.h" #include "compat/bionic_pthread_create_runtime.h" +#include "compat/bionic_socket_runtime.h" #include "compat/build_profile.h" #include "compat/elf_build_id.h" #include "compat/host_abi_experiment.h" @@ -32804,6 +32806,12 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, } RegisterBionicDnsWrappers(); RegisterBionicPathWrappers(); + linker::RegisterSymbol("prctl", + reinterpret_cast(mocktail_bionic_prctl)); + linker::RegisterSymbol( + "setsockopt", reinterpret_cast(mocktail_bionic_setsockopt)); + linker::RegisterSymbol("sendmsg", + reinterpret_cast(mocktail_bionic_sendmsg)); linker::RegisterSymbol("mprotect", reinterpret_cast(mocktail_mprotect)); linker::RegisterSymbol("pthread_condattr_init", reinterpret_cast(mocktail_pthread_condattr_init)); diff --git a/src/linker/linker.cc b/src/linker/linker.cc index 64b0283..a911641 100644 --- a/src/linker/linker.cc +++ b/src/linker/linker.cc @@ -134,11 +134,13 @@ bool IsBionicLibcOnlySymbol(std::string_view symbol) { } return symbol == "__cmsg_nxthdr" || symbol == "__cxa_thread_atexit_impl" || symbol == "__readlink_chk" || symbol == "__register_atfork" || - symbol == "arc4random_buf" || symbol == "lseek64" || + symbol == "arc4random_buf" || symbol == "geteuid" || + symbol == "getuid" || symbol == "lseek64" || symbol == "mallinfo" || symbol == "pread64" || symbol == "pwrite64" || symbol == "sigaction" || symbol == "strerror_r" || symbol == "strtoll_l" || symbol == "strtoull_l" || - symbol == "sysconf" || HasPrefix(symbol, "pthread_") || + symbol == "sysconf" || symbol == "sysinfo" || symbol == "uname" || + HasPrefix(symbol, "pthread_") || HasPrefix(symbol, "__pthread_") || HasPrefix(symbol, "sem_"); } @@ -360,6 +362,18 @@ void RegisterBionicHostLibcRuntimeForLibc() { mocktail_bionic_arc4random_buf)); RegisterSyntheticSymbol("libc.so", "mallinfo", reinterpret_cast(mocktail_bionic_mallinfo)); + RegisterSyntheticSymbol("libc.so", "sysinfo", + reinterpret_cast(mocktail_bionic_sysinfo)); + RegisterSyntheticSymbol("libc.so", "uname", + reinterpret_cast(mocktail_bionic_uname)); + RegisterSyntheticSymbol("libc.so", "getuid", + reinterpret_cast(mocktail_bionic_getuid)); + RegisterSyntheticSymbol("libc.so", "geteuid", + reinterpret_cast(mocktail_bionic_geteuid)); + RegisterSymbol("sysinfo", reinterpret_cast(mocktail_bionic_sysinfo)); + RegisterSymbol("uname", reinterpret_cast(mocktail_bionic_uname)); + RegisterSymbol("getuid", reinterpret_cast(mocktail_bionic_getuid)); + RegisterSymbol("geteuid", reinterpret_cast(mocktail_bionic_geteuid)); RegisterSyntheticSymbol("libc.so", "strerror_r", reinterpret_cast(mocktail_bionic_strerror_r)); // Publish this before relocation to prevent binding glibc's incompatible diff --git a/src/runtime/payload_update_preflight.cc b/src/runtime/payload_update_preflight.cc index e55dcb6..606c940 100644 --- a/src/runtime/payload_update_preflight.cc +++ b/src/runtime/payload_update_preflight.cc @@ -63,8 +63,14 @@ std::filesystem::path UpdateHelper(const Environment& environment) { if (!root.is_absolute()) { return {}; } - const std::filesystem::path helper = root / "build" / "mocktail_updater"; - return IsExecutableRegularFile(helper) ? helper : std::filesystem::path{}; + for (const std::filesystem::path& helper : + {root / "build" / "mocktail_updater", + root / "bin" / "mocktail_updater"}) { + if (IsExecutableRegularFile(helper)) { + return helper; + } + } + return {}; } std::error_code error; diff --git a/src/runtime/runtime_paths.cc b/src/runtime/runtime_paths.cc index 6646bf0..5c9e01c 100644 --- a/src/runtime/runtime_paths.cc +++ b/src/runtime/runtime_paths.cc @@ -15,6 +15,8 @@ #include "runtime/runtime_paths.h" #define JSON_NOEXCEPTION 1 +#include +#include #include #include #include @@ -22,6 +24,8 @@ #include #include #include +#include +#include namespace mocktail { namespace runtime { @@ -170,6 +174,129 @@ bool SetEnvironmentDefault(const char* name, return false; } +struct ManagedPayloadBinding { + std::filesystem::path target; + std::filesystem::path link; + const char* name; + bool directory; +}; + +bool ResolveManagedPayloadBinding(ManagedPayloadBinding* binding, + std::string* error) { + std::error_code filesystem_error; + const std::filesystem::file_status target_status = + std::filesystem::symlink_status(binding->target, filesystem_error); + const bool expected_type = + binding->directory ? std::filesystem::is_directory(target_status) + : std::filesystem::is_regular_file(target_status); + if (filesystem_error || !expected_type || + std::filesystem::is_symlink(target_status)) { + if (error != nullptr) { + *error = "managed payload " + std::string(binding->name) + + " target is missing, invalid, or a symlink: " + + binding->target.string(); + } + return false; + } + binding->target = + std::filesystem::canonical(binding->target, filesystem_error); + if (filesystem_error) { + if (error != nullptr) { + *error = "cannot canonicalize managed payload " + + std::string(binding->name) + " target: " + + binding->target.string(); + } + return false; + } + + const std::filesystem::file_status link_status = + std::filesystem::symlink_status(binding->link, filesystem_error); + if (filesystem_error == std::errc::no_such_file_or_directory) { + filesystem_error.clear(); + return true; + } + if (!filesystem_error && + link_status.type() == std::filesystem::file_type::not_found) { + return true; + } + if (filesystem_error) { + if (error != nullptr) { + *error = "cannot inspect managed payload " + + std::string(binding->name) + " path: " + + binding->link.string(); + } + return false; + } + if (!std::filesystem::is_symlink(link_status)) { + if (error != nullptr) { + *error = "refusing to replace non-symlink managed payload " + + std::string(binding->name) + " path: " + + binding->link.string(); + } + return false; + } + return true; +} + +bool PublishManagedPayloadBinding(const ManagedPayloadBinding& binding, + std::string* error) { + std::error_code filesystem_error; + const std::filesystem::path current_target = + std::filesystem::read_symlink(binding.link, filesystem_error); + if (!filesystem_error && current_target == binding.target) { + return true; + } + if (filesystem_error == std::errc::no_such_file_or_directory) { + filesystem_error.clear(); + } else if (filesystem_error) { + if (error != nullptr) { + *error = "cannot read managed payload " + std::string(binding.name) + + " link: " + binding.link.string(); + } + return false; + } + + static std::atomic_uint64_t sequence{0}; + const std::filesystem::path temporary_link = + binding.link.string() + ".mocktail-next-" + + std::to_string(static_cast(getpid())) + "-" + + std::to_string(sequence.fetch_add(1, std::memory_order_relaxed)); + const std::filesystem::file_status temporary_status = + std::filesystem::symlink_status(temporary_link, filesystem_error); + if (filesystem_error == std::errc::no_such_file_or_directory || + (!filesystem_error && temporary_status.type() == + std::filesystem::file_type::not_found)) { + filesystem_error.clear(); + } else { + if (error != nullptr) { + *error = "managed payload temporary link already exists: " + + temporary_link.string(); + } + return false; + } + + if (binding.directory) { + std::filesystem::create_directory_symlink(binding.target, temporary_link, + filesystem_error); + } else { + std::filesystem::create_symlink(binding.target, temporary_link, + filesystem_error); + } + if (!filesystem_error) { + std::filesystem::rename(temporary_link, binding.link, filesystem_error); + } + if (filesystem_error) { + std::error_code cleanup_error; + std::filesystem::remove(temporary_link, cleanup_error); + if (error != nullptr) { + *error = "cannot publish managed payload " + std::string(binding.name) + + " link: " + binding.link.string(); + } + return false; + } + return true; +} + } // namespace RuntimePaths RuntimePaths::FromEnvironment( @@ -329,6 +456,10 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { } const std::filesystem::path library = canonical_root / "libroblox.so"; + const std::filesystem::path package_root = canonical_root / "sober_apk"; + const std::filesystem::path base_apk = package_root / "base.apk"; + const std::filesystem::path x86_64_split_apk = + package_root / "split_config.x86_64.apk"; const std::filesystem::path assets_root = canonical_root / "assets"; const std::filesystem::path assets = canonical_root / "assets/content"; const std::filesystem::file_status library_status = @@ -338,6 +469,27 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { result.error = "active payload libroblox.so is missing or invalid"; return result; } + const std::filesystem::file_status package_root_status = + std::filesystem::symlink_status(package_root, filesystem_error); + if (filesystem_error || + !std::filesystem::is_directory(package_root_status) || + std::filesystem::is_symlink(package_root_status)) { + result.error = "active payload sober_apk root is missing or invalid"; + return result; + } + for (const auto& [path, name] : + std::array, 2>{ + std::pair{base_apk, "base.apk"}, + std::pair{x86_64_split_apk, "split_config.x86_64.apk"}}) { + const std::filesystem::file_status status = + std::filesystem::symlink_status(path, filesystem_error); + if (filesystem_error || !std::filesystem::is_regular_file(status) || + std::filesystem::is_symlink(status)) { + result.error = "active payload " + std::string(name) + + " is missing or invalid"; + return result; + } + } const std::filesystem::file_status assets_root_status = std::filesystem::symlink_status(assets_root, filesystem_error); if (filesystem_error || !std::filesystem::is_directory(assets_root_status) || @@ -356,6 +508,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { result.active = true; result.root = canonical_root; result.roblox_library = library; + result.base_apk = base_apk; + result.x86_64_split_apk = x86_64_split_apk; result.assets_content = assets; const auto compatibility_manifest = @@ -488,45 +642,37 @@ bool PrepareManagedPayloadWorkingDirectory(const RuntimePaths& paths, } return false; } - - const std::filesystem::path assets_link = rbx_root / "assets"; - const std::filesystem::file_status link_status = - std::filesystem::symlink_status(assets_link, filesystem_error); - if (filesystem_error == std::errc::no_such_file_or_directory) { - filesystem_error.clear(); - } else if (filesystem_error) { + const std::filesystem::file_status rbx_root_status = + std::filesystem::symlink_status(rbx_root, filesystem_error); + if (filesystem_error || !std::filesystem::is_directory(rbx_root_status) || + std::filesystem::is_symlink(rbx_root_status)) { if (error != nullptr) { - *error = - "cannot inspect managed payload asset link: " + assets_link.string(); - } - return false; - } else if (std::filesystem::is_symlink(link_status)) { - std::filesystem::remove(assets_link, filesystem_error); - } else if (!std::filesystem::is_directory(link_status)) { - if (error != nullptr) { - *error = "managed payload asset path is not a directory: " + - assets_link.string(); - } - return false; - } else { - std::filesystem::current_path(paths.data_root(), filesystem_error); - if (!filesystem_error) { - return true; - } - } - if (filesystem_error) { - if (error != nullptr) { - *error = - "cannot replace managed payload asset link: " + assets_link.string(); + *error = "managed payload rbx_bin root is invalid or a symlink: " + + rbx_root.string(); } return false; } - std::filesystem::create_directory_symlink(active.root / "assets", assets_link, - filesystem_error); - if (!filesystem_error) { - std::filesystem::current_path(paths.data_root(), filesystem_error); + std::array bindings{{ + {active.root / "assets", rbx_root / "assets", "assets", true}, + {active.root / "sober_apk", rbx_root / "sober_apk", "APK directory", + true}, + {active.root / "libroblox.so", rbx_root / "libroblox.so", + "native library", false}, + }}; + // Validate all destinations before publishing any of them. + for (ManagedPayloadBinding& binding : bindings) { + if (!ResolveManagedPayloadBinding(&binding, error)) { + return false; + } } + for (const ManagedPayloadBinding& binding : bindings) { + if (!PublishManagedPayloadBinding(binding, error)) { + return false; + } + } + + std::filesystem::current_path(paths.data_root(), filesystem_error); if (filesystem_error) { if (error != nullptr) { *error = "cannot activate managed payload working directory: " + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5d04170..9db44c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -964,7 +964,7 @@ add_test( ) find_program(MOCKTAIL_PATCHELF_EXECUTABLE patchelf) -if(MOCKTAIL_PATCHELF_EXECUTABLE) +if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER AND MOCKTAIL_PATCHELF_EXECUTABLE) add_test( NAME PortablePackagingTest.RelocatableReadOnlyBundle COMMAND ${CMAKE_COMMAND} -E env @@ -1003,8 +1003,10 @@ add_test( ${CMAKE_SOURCE_DIR} ) +if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER) add_test( NAME SystemInstallTest.DynamicRuntimeLayout COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/system_install_test.sh ${CMAKE_BINARY_DIR} ) +endif() diff --git a/tests/bionic_host_libc_runtime_test.cc b/tests/bionic_host_libc_runtime_test.cc index 99b5548..218eec7 100644 --- a/tests/bionic_host_libc_runtime_test.cc +++ b/tests/bionic_host_libc_runtime_test.cc @@ -53,8 +53,8 @@ struct DestructorProbe { std::atomic calls{0}; }; -void RecordThreadDestructor(void* argument) { - auto* probe = static_cast(argument); +void RecordThreadDestructor(void *argument) { + auto *probe = static_cast(argument); probe->calls.fetch_add(1, std::memory_order_relaxed); } @@ -68,10 +68,10 @@ TEST(BionicHostLibcRuntimeTest, WalksValidControlMessages) { message.msg_control = control.data(); message.msg_controllen = control.size(); - auto* first = reinterpret_cast(control.data()); + auto *first = reinterpret_cast
(control.data()); first->cmsg_len = ControlMessageLength(kPayloadSize); - auto* second = reinterpret_cast(control.data() + - ControlMessageSpace(kPayloadSize)); + auto *second = reinterpret_cast
(control.data() + + ControlMessageSpace(kPayloadSize)); second->cmsg_len = ControlMessageLength(kPayloadSize); EXPECT_EQ(mocktail_bionic_cmsg_nxthdr(&message, first), second); @@ -84,7 +84,7 @@ TEST(BionicHostLibcRuntimeTest, RejectsMalformedControlMessages) { mocktail::compat::BionicMessageHeader message{}; message.msg_control = control.data(); message.msg_controllen = control.size(); - auto* header = reinterpret_cast(control.data()); + auto *header = reinterpret_cast
(control.data()); header->cmsg_len = sizeof(Header) - 1; EXPECT_EQ(mocktail_bionic_cmsg_nxthdr(&message, header), nullptr); @@ -145,6 +145,80 @@ TEST(BionicHostLibcRuntimeTest, ReturnsAbiSizedAllocatorSnapshot) { } } +TEST(BionicHostLibcRuntimeTest, PreservesValidSysInfoCounters) { + struct sysinfo info{}; + info.totalram = 8UL * 1024UL * 1024UL * 1024UL; + info.freeram = 2UL * 1024UL * 1024UL * 1024UL; + info.totalswap = 4UL * 1024UL * 1024UL * 1024UL; + info.freeswap = 1UL * 1024UL * 1024UL * 1024UL; + + mocktail::compat::NormalizeBionicSysInfo(&info); + + EXPECT_EQ(info.totalram, 8UL * 1024UL * 1024UL * 1024UL); + EXPECT_EQ(info.freeram, 2UL * 1024UL * 1024UL * 1024UL); + EXPECT_EQ(info.totalswap, 4UL * 1024UL * 1024UL * 1024UL); + EXPECT_EQ(info.freeswap, 1UL * 1024UL * 1024UL * 1024UL); +} + +TEST(BionicHostLibcRuntimeTest, ClearsLinuxulatorUnderflowedSwapCounters) { + struct sysinfo info{}; + info.totalram = 8UL * 1024UL * 1024UL * 1024UL; + info.freeram = 2UL * 1024UL * 1024UL * 1024UL; + info.totalswap = std::numeric_limits::max() - 40959UL; + info.freeswap = std::numeric_limits::max() - 11550719UL; + + mocktail::compat::NormalizeBionicSysInfo(&info); + + EXPECT_EQ(info.totalram, 8UL * 1024UL * 1024UL * 1024UL); + EXPECT_EQ(info.freeram, 2UL * 1024UL * 1024UL * 1024UL); + EXPECT_EQ(info.totalswap, 0UL); + EXPECT_EQ(info.freeswap, 0UL); +} + +TEST(BionicHostLibcRuntimeTest, CallsHostSysInfoThroughBionicAdapter) { + struct sysinfo info{}; + ASSERT_EQ(mocktail_bionic_sysinfo(&info), 0); + EXPECT_GT(info.totalram, 0UL); + EXPECT_LE(info.freeswap, info.totalswap); +} + +TEST(BionicHostLibcRuntimeTest, NormalizesContradictoryLinuxulatorUname) { + struct utsname name{}; + std::snprintf(name.sysname, sizeof(name.sysname), "Linux"); + std::snprintf(name.release, sizeof(name.release), "5.15.0"); + std::snprintf(name.version, sizeof(name.version), + "FreeBSD 15.1-RELEASE-p2 GENERIC"); + + EXPECT_TRUE(mocktail::compat::NormalizeLinuxulatorUnameVersion( + &name, "#4 SMP PREEMPT_DYNAMIC")); + EXPECT_STREQ(name.sysname, "Linux"); + EXPECT_STREQ(name.release, "5.15.0"); + EXPECT_STREQ(name.version, "#4 SMP PREEMPT_DYNAMIC"); +} + +TEST(BionicHostLibcRuntimeTest, PreservesNativeLinuxUname) { + struct utsname name{}; + std::snprintf(name.sysname, sizeof(name.sysname), "Linux"); + std::snprintf(name.version, sizeof(name.version), "#1 SMP PREEMPT_DYNAMIC"); + + EXPECT_FALSE(mocktail::compat::NormalizeLinuxulatorUnameVersion( + &name, "#4 SMP PREEMPT_DYNAMIC")); + EXPECT_STREQ(name.version, "#1 SMP PREEMPT_DYNAMIC"); + + ASSERT_EQ(mocktail_bionic_uname(&name), 0); + EXPECT_STREQ(name.sysname, "Linux"); +} + +TEST(BionicHostLibcRuntimeTest, HidesPrivilegedHostIdentityFromAndroidGuest) { + EXPECT_EQ(mocktail::compat::NormalizeBionicApplicationUid(0), 1000U); + EXPECT_EQ(mocktail::compat::NormalizeBionicApplicationUid(1000), 1000U); + EXPECT_EQ(mocktail::compat::NormalizeBionicApplicationUid(12345), 12345U); + EXPECT_EQ(mocktail_bionic_getuid(), + mocktail::compat::NormalizeBionicApplicationUid(::getuid())); + EXPECT_EQ(mocktail_bionic_geteuid(), + mocktail::compat::NormalizeBionicApplicationUid(::geteuid())); +} + TEST(BionicHostLibcRuntimeTest, UsesBionicPosixStrErrorContract) { std::array buffer{}; @@ -175,7 +249,7 @@ TEST(BionicHostLibcRuntimeTest, ParsesIntegersInScopedCLocale) { auto guest_locale = reinterpret_cast(uintptr_t{1}); - char* signed_end = nullptr; + char *signed_end = nullptr; errno = EDOM; EXPECT_EQ( mocktail_bionic_strtoll_l("-0x7f-rest", &signed_end, 0, guest_locale), @@ -183,7 +257,7 @@ TEST(BionicHostLibcRuntimeTest, ParsesIntegersInScopedCLocale) { EXPECT_STREQ(signed_end, "-rest"); EXPECT_EQ(errno, EDOM); - char* unsigned_end = nullptr; + char *unsigned_end = nullptr; errno = 0; EXPECT_EQ(mocktail_bionic_strtoull_l("18446744073709551616!", &unsigned_end, 10, guest_locale), @@ -215,12 +289,11 @@ TEST(BionicHostLibcRuntimeTest, FortifiedReadlinkAbortsAboveSsizeLimit) { EXPECT_DEATH( { char buffer{}; - mocktail___readlink_chk( - "/proc/self/exe", &buffer, - static_cast(SSIZE_MAX) + size_t{1}, - std::numeric_limits::max()); + mocktail___readlink_chk("/proc/self/exe", &buffer, + static_cast(SSIZE_MAX) + size_t{1}, + std::numeric_limits::max()); }, "FORTIFY: readlink"); } -} // namespace +} // namespace diff --git a/tests/bionic_sysconf_test.cc b/tests/bionic_sysconf_test.cc index 799cedc..d0e29b3 100644 --- a/tests/bionic_sysconf_test.cc +++ b/tests/bionic_sysconf_test.cc @@ -16,21 +16,20 @@ TEST(BionicSysconfTest, TranslatesPageSizeAliases) { EXPECT_EQ(BionicSysconf(static_cast(BionicSysconfName::kPageSize)), host_page_size); - EXPECT_EQ( - BionicSysconf(static_cast(BionicSysconfName::kPageSizeAlias)), - host_page_size); + EXPECT_EQ(BionicSysconf(static_cast(BionicSysconfName::kPageSizeAlias)), + host_page_size); EXPECT_GT(host_page_size, 0); EXPECT_EQ(host_page_size & (host_page_size - 1), 0); } TEST(BionicSysconfTest, TranslatesObservedCapacityNames) { EXPECT_EQ(BionicSysconf(static_cast(BionicSysconfName::kClockTicks)), - ::sysconf(_SC_CLK_TCK)); + 100); EXPECT_EQ(BionicSysconf(static_cast(BionicSysconfName::kIovMax)), ::sysconf(_SC_IOV_MAX)); - EXPECT_EQ(BionicSysconf( - static_cast(BionicSysconfName::kProcessorsConfigured)), - ::sysconf(_SC_NPROCESSORS_CONF)); + EXPECT_EQ( + BionicSysconf(static_cast(BionicSysconfName::kProcessorsConfigured)), + ::sysconf(_SC_NPROCESSORS_CONF)); EXPECT_EQ( BionicSysconf(static_cast(BionicSysconfName::kProcessorsOnline)), ::sysconf(_SC_NPROCESSORS_ONLN)); @@ -45,5 +44,5 @@ TEST(BionicSysconfTest, RejectsUnknownBionicName) { EXPECT_EQ(errno, EINVAL); } -} // namespace -} // namespace mocktail::compat +} // namespace +} // namespace mocktail::compat diff --git a/tests/flatpak_packaging_test.sh b/tests/flatpak_packaging_test.sh index fbe95fb..2b36f9c 100755 --- a/tests/flatpak_packaging_test.sh +++ b/tests/flatpak_packaging_test.sh @@ -98,6 +98,7 @@ assert project_sources == [ ] mocktail_options = set(modules["mocktail"]["config-opts"]) assert "-DBUILD_TESTING=OFF" in mocktail_options +assert "-DMOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER=OFF" in mocktail_options assert "-DCMAKE_INSTALL_LIBDIR=lib" in mocktail_options assert ( "-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=" diff --git a/tests/linker_registry_test.cc b/tests/linker_registry_test.cc index df97af6..9331d59 100644 --- a/tests/linker_registry_test.cc +++ b/tests/linker_registry_test.cc @@ -33,8 +33,8 @@ TEST(LinkerRegistryTest, KeepsSyntheticExportsOwnedBySoname) { linker::RegisterSyntheticSymbol("libmocktail_test_log.so", "sharedName", &log_export); - linker::SymbolMap android_symbols = linker::GetSyntheticLibrarySymbols( - "libmocktail_test_android.so"); + linker::SymbolMap android_symbols = + linker::GetSyntheticLibrarySymbols("libmocktail_test_android.so"); linker::SymbolMap log_symbols = linker::GetSyntheticLibrarySymbols("libmocktail_test_log.so"); @@ -64,31 +64,31 @@ TEST(LinkerRegistryTest, KeepsBionicPthreadKeyAbiOwnedByLibc) { const linker::SymbolMap symbols = linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("pthread_key_create"), - reinterpret_cast(mocktail_bionic_pthread_key_create)); + reinterpret_cast(mocktail_bionic_pthread_key_create)); EXPECT_EQ(symbols.at("pthread_key_delete"), - reinterpret_cast(mocktail_bionic_pthread_key_delete)); + reinterpret_cast(mocktail_bionic_pthread_key_delete)); EXPECT_EQ(symbols.at("pthread_getspecific"), - reinterpret_cast(mocktail_bionic_pthread_getspecific)); + reinterpret_cast(mocktail_bionic_pthread_getspecific)); EXPECT_EQ(symbols.at("pthread_setspecific"), - reinterpret_cast(mocktail_bionic_pthread_setspecific)); + reinterpret_cast(mocktail_bionic_pthread_setspecific)); EXPECT_EQ(symbols.at("sem_init"), - reinterpret_cast(mocktail_bionic_sem_init)); + reinterpret_cast(mocktail_bionic_sem_init)); EXPECT_EQ(symbols.at("sem_destroy"), - reinterpret_cast(mocktail_bionic_sem_destroy)); + reinterpret_cast(mocktail_bionic_sem_destroy)); EXPECT_EQ(symbols.at("sem_wait"), - reinterpret_cast(mocktail_bionic_sem_wait)); + reinterpret_cast(mocktail_bionic_sem_wait)); EXPECT_EQ(symbols.at("sem_post"), - reinterpret_cast(mocktail_bionic_sem_post)); + reinterpret_cast(mocktail_bionic_sem_post)); EXPECT_EQ(symbols.at("pthread_rwlock_init"), - reinterpret_cast(mocktail_bionic_pthread_rwlock_init)); + reinterpret_cast(mocktail_bionic_pthread_rwlock_init)); EXPECT_EQ(symbols.at("pthread_rwlock_destroy"), - reinterpret_cast(mocktail_bionic_pthread_rwlock_destroy)); + reinterpret_cast(mocktail_bionic_pthread_rwlock_destroy)); EXPECT_EQ(symbols.at("pthread_rwlock_rdlock"), - reinterpret_cast(mocktail_bionic_pthread_rwlock_rdlock)); + reinterpret_cast(mocktail_bionic_pthread_rwlock_rdlock)); EXPECT_EQ(symbols.at("pthread_rwlock_wrlock"), - reinterpret_cast(mocktail_bionic_pthread_rwlock_wrlock)); + reinterpret_cast(mocktail_bionic_pthread_rwlock_wrlock)); EXPECT_EQ(symbols.at("pthread_rwlock_unlock"), - reinterpret_cast(mocktail_bionic_pthread_rwlock_unlock)); + reinterpret_cast(mocktail_bionic_pthread_rwlock_unlock)); } TEST(LinkerRegistryTest, KeepsBionicAtForkAbiOwnedByLibc) { @@ -97,7 +97,7 @@ TEST(LinkerRegistryTest, KeepsBionicAtForkAbiOwnedByLibc) { const linker::SymbolMap symbols = linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("__register_atfork"), - reinterpret_cast(mocktail_bionic_register_atfork)); + reinterpret_cast(mocktail_bionic_register_atfork)); } TEST(LinkerRegistryTest, KeepsHostIndependentBionicAbiOwnedByLibc) { @@ -106,36 +106,51 @@ TEST(LinkerRegistryTest, KeepsHostIndependentBionicAbiOwnedByLibc) { const linker::SymbolMap symbols = linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("getaddrinfo"), - reinterpret_cast(mocktail_bionic_getaddrinfo)); + reinterpret_cast(mocktail_bionic_getaddrinfo)); EXPECT_EQ(symbols.at("freeaddrinfo"), - reinterpret_cast(mocktail_bionic_freeaddrinfo)); + reinterpret_cast(mocktail_bionic_freeaddrinfo)); EXPECT_EQ(symbols.at("gethostbyname"), - reinterpret_cast(mocktail_bionic_gethostbyname)); + reinterpret_cast(mocktail_bionic_gethostbyname)); EXPECT_EQ(symbols.at("__cmsg_nxthdr"), - reinterpret_cast(mocktail_bionic_cmsg_nxthdr)); - EXPECT_EQ( - symbols.at("__cxa_thread_atexit_impl"), - reinterpret_cast(mocktail_bionic_cxa_thread_atexit_impl)); + reinterpret_cast(mocktail_bionic_cmsg_nxthdr)); + EXPECT_EQ(symbols.at("__cxa_thread_atexit_impl"), + reinterpret_cast(mocktail_bionic_cxa_thread_atexit_impl)); EXPECT_EQ(symbols.at("__readlink_chk"), - reinterpret_cast(mocktail___readlink_chk)); + reinterpret_cast(mocktail___readlink_chk)); EXPECT_EQ(symbols.at("arc4random_buf"), - reinterpret_cast(mocktail_bionic_arc4random_buf)); + reinterpret_cast(mocktail_bionic_arc4random_buf)); EXPECT_EQ(symbols.at("mallinfo"), - reinterpret_cast(mocktail_bionic_mallinfo)); + reinterpret_cast(mocktail_bionic_mallinfo)); + EXPECT_EQ(symbols.at("sysinfo"), + reinterpret_cast(mocktail_bionic_sysinfo)); + EXPECT_EQ(symbols.at("uname"), + reinterpret_cast(mocktail_bionic_uname)); + EXPECT_EQ(symbols.at("getuid"), + reinterpret_cast(mocktail_bionic_getuid)); + EXPECT_EQ(symbols.at("geteuid"), + reinterpret_cast(mocktail_bionic_geteuid)); EXPECT_EQ(symbols.at("strerror_r"), - reinterpret_cast(mocktail_bionic_strerror_r)); + reinterpret_cast(mocktail_bionic_strerror_r)); EXPECT_EQ(symbols.at("strtoll_l"), - reinterpret_cast(mocktail_bionic_strtoll_l)); + reinterpret_cast(mocktail_bionic_strtoll_l)); EXPECT_EQ(symbols.at("strtoull_l"), - reinterpret_cast(mocktail_bionic_strtoull_l)); + reinterpret_cast(mocktail_bionic_strtoull_l)); EXPECT_EQ(linker::GetBionicSymbols().at("strerror_r"), - reinterpret_cast(mocktail_bionic_strerror_r)); + reinterpret_cast(mocktail_bionic_strerror_r)); EXPECT_EQ(linker::GetBionicSymbols().at("getaddrinfo"), - reinterpret_cast(mocktail_bionic_getaddrinfo)); + reinterpret_cast(mocktail_bionic_getaddrinfo)); EXPECT_EQ(linker::GetBionicSymbols().at("freeaddrinfo"), - reinterpret_cast(mocktail_bionic_freeaddrinfo)); + reinterpret_cast(mocktail_bionic_freeaddrinfo)); EXPECT_EQ(linker::GetBionicSymbols().at("gethostbyname"), - reinterpret_cast(mocktail_bionic_gethostbyname)); + reinterpret_cast(mocktail_bionic_gethostbyname)); + EXPECT_EQ(linker::GetBionicSymbols().at("getuid"), + reinterpret_cast(mocktail_bionic_getuid)); + EXPECT_EQ(linker::GetBionicSymbols().at("geteuid"), + reinterpret_cast(mocktail_bionic_geteuid)); + EXPECT_EQ(linker::GetBionicSymbols().at("sysinfo"), + reinterpret_cast(mocktail_bionic_sysinfo)); + EXPECT_EQ(linker::GetBionicSymbols().at("uname"), + reinterpret_cast(mocktail_bionic_uname)); } TEST(LinkerRegistryTest, KeepsBionicLargeFileAbiOwnedByLibc) { @@ -144,11 +159,11 @@ TEST(LinkerRegistryTest, KeepsBionicLargeFileAbiOwnedByLibc) { const linker::SymbolMap symbols = linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("pread64"), - reinterpret_cast(mocktail_bionic_pread64)); + reinterpret_cast(mocktail_bionic_pread64)); EXPECT_EQ(symbols.at("pwrite64"), - reinterpret_cast(mocktail_bionic_pwrite64)); + reinterpret_cast(mocktail_bionic_pwrite64)); EXPECT_EQ(symbols.at("lseek64"), - reinterpret_cast(mocktail_bionic_lseek64)); + reinterpret_cast(mocktail_bionic_lseek64)); } TEST(LinkerRegistryTest, KeepsBionicSysconfAbiOwnedByLibc) { @@ -157,7 +172,7 @@ TEST(LinkerRegistryTest, KeepsBionicSysconfAbiOwnedByLibc) { const linker::SymbolMap symbols = linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("sysconf"), - reinterpret_cast(mocktail_bionic_sysconf)); + reinterpret_cast(mocktail_bionic_sysconf)); } TEST(LinkerRegistryTest, KeepsBionicStdioAbiOwnedByLibc) { @@ -167,18 +182,15 @@ TEST(LinkerRegistryTest, KeepsBionicStdioAbiOwnedByLibc) { linker::GetSyntheticLibrarySymbols("libc.so"); EXPECT_EQ(symbols.at("__sF"), mocktail::compat::BionicFileArraySymbolAddress()); - EXPECT_EQ(symbols.at("stdin"), - mocktail::compat::BionicStdinSymbolAddress()); + EXPECT_EQ(symbols.at("stdin"), mocktail::compat::BionicStdinSymbolAddress()); EXPECT_EQ(symbols.at("stdout"), mocktail::compat::BionicStdoutSymbolAddress()); EXPECT_EQ(symbols.at("stderr"), mocktail::compat::BionicStderrSymbolAddress()); - EXPECT_EQ(symbols.at("fflush"), - reinterpret_cast(mocktail_fflush)); - EXPECT_EQ(symbols.at("fscanf"), - reinterpret_cast(mocktail_fscanf)); + EXPECT_EQ(symbols.at("fflush"), reinterpret_cast(mocktail_fflush)); + EXPECT_EQ(symbols.at("fscanf"), reinterpret_cast(mocktail_fscanf)); EXPECT_EQ(symbols.at("__fwrite_chk"), - reinterpret_cast(mocktail___fwrite_chk)); + reinterpret_cast(mocktail___fwrite_chk)); } TEST(LinkerRegistryTest, KeepsBionicDynamicLoaderOwnedByLibdl) { @@ -211,4 +223,4 @@ TEST(LinkerRegistryTest, WithholdsEveryCrashpadJniEntrypoint) { "nativeAppBridgeV2InitWithParams")); } -} // namespace +} // namespace diff --git a/tests/payload_update_preflight_test.cc b/tests/payload_update_preflight_test.cc index fdb83ea..fdf664c 100644 --- a/tests/payload_update_preflight_test.cc +++ b/tests/payload_update_preflight_test.cc @@ -104,6 +104,27 @@ TEST(PayloadUpdatePreflightTest, RunsNativeUpdater) { EXPECT_TRUE(result.attempted); } +TEST(PayloadUpdatePreflightTest, RunsPackagedUpdaterFromProjectBin) { + TemporaryDirectory temporary; + const std::filesystem::path project_root = temporary.root() / "portable"; + const std::filesystem::path helper = project_root / "bin/mocktail_updater"; + ASSERT_TRUE(std::filesystem::create_directories(helper.parent_path())); + ASSERT_TRUE(std::filesystem::copy_file(MOCKTAIL_TEST_UPDATE_HELPER, helper)); + std::filesystem::permissions( + helper, + std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | + std::filesystem::perms::others_exec, + std::filesystem::perm_options::add); + const MapEnvironment environment( + {{"MOCKTAIL_PROJECT_ROOT", project_root.string()}}); + const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); + + const auto result = RunPayloadUpdatePreflight(environment, paths); + + ASSERT_TRUE(result) << result.error; + EXPECT_TRUE(result.attempted); +} + TEST(PayloadUpdatePreflightTest, StreamsUpdaterStagesToNativeDialog) { TemporaryDirectory temporary; const std::filesystem::path capture = temporary.root() / "progress"; diff --git a/tests/portable_packaging_test.sh b/tests/portable_packaging_test.sh index 93309f0..d096540 100755 --- a/tests/portable_packaging_test.sh +++ b/tests/portable_packaging_test.sh @@ -23,7 +23,7 @@ grep -q '^artifact_name=mocktail-linux-x86_64-glibc-standalone$' \ <<<"${dry_run}" grep -q '^interpreter=.*/ld-linux-x86-64\.so\.2$' <<<"${dry_run}" grep -q '^android_tools_libc=glibc$' <<<"${dry_run}" -grep -q '^project_artifacts=16$' <<<"${dry_run}" +grep -q '^project_artifacts=17$' <<<"${dry_run}" grep -q 'host=libc\.so\.6 reason=glibc' <<<"${dry_run}" grep -q 'host=libEGL\.so\.1 reason=gpu-driver-stack' <<<"${dry_run}" ! grep -q 'host=libwebkitgtk-6\.0\.so\.4' <<<"${dry_run}" @@ -114,8 +114,10 @@ RefreshBundleChecksums() { for path in \ run.sh \ mocktail/bin/mocktail \ + mocktail/bin/mocktail_updater \ mocktail/bin/mocktail_failure_dialog \ mocktail/bin/mocktail_webview_helper \ + mocktail/bin/mocktail_freebsd_socket_helper \ mocktail/bin/libEGL.so \ mocktail/bin/libvulkan.so \ mocktail/runtime \ @@ -164,6 +166,11 @@ runtime_entries="$(find "${runtime}" -mindepth 1 -maxdepth 1 \ mocktail/metadata/SHA256SUMS.txt) "${runtime}/runtime/android-tools/bin/aapt" version >/dev/null "${runtime}/runtime/android-tools/bin/apksigner" version >/dev/null +LC_ALL=C readelf -h "${runtime}/bin/mocktail_freebsd_socket_helper" | + grep -Fq 'UNIX - FreeBSD' +[[ -z "$(LC_ALL=C readelf -l \ + "${runtime}/bin/mocktail_freebsd_socket_helper" | + sed -n 's/.*Requesting program interpreter: \([^]]*\).*/\1/p')" ]] [[ "$(stat -c '%a' "${bundle}")" == 755 ]] packaged_root_entries="$(find "${bundle}" -mindepth 1 -maxdepth 1 \ -printf '%f\n' | LC_ALL=C sort)" @@ -262,6 +269,8 @@ int main(void) { EOF cp -- "${musl_runtime}/bin/mocktail" \ "${musl_runtime}/bin/mocktail_failure_dialog" + cp -- "${musl_runtime}/bin/mocktail" \ + "${musl_runtime}/bin/mocktail_updater" cp -- "${musl_runtime}/bin/mocktail" \ "${musl_runtime}/bin/mocktail_webview_helper" for tool in aapt apkanalyzer apksigner; do diff --git a/tests/runtime_paths_test.cc b/tests/runtime_paths_test.cc index acb7175..50fdbf5 100644 --- a/tests/runtime_paths_test.cc +++ b/tests/runtime_paths_test.cc @@ -102,6 +102,15 @@ class ScopedCurrentPath { std::filesystem::path original_; }; +void WriteManagedPayloadFiles(const std::filesystem::path& payload) { + ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content")); + ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "sober_apk")); + std::ofstream(payload / "libroblox.so") << "ELF fixture"; + std::ofstream(payload / "sober_apk/base.apk") << "base APK fixture"; + std::ofstream(payload / "sober_apk/split_config.x86_64.apk") + << "x86_64 split APK fixture"; +} + TEST(RuntimePathsTest, DerivesXdgDefaults) { const MapEnvironment environment({{"HOME", "/home/mocktail"}}); const RuntimePaths paths = @@ -162,8 +171,7 @@ TEST(RuntimePathsTest, ResolvesValidatedActivePayload) { const std::string payload_id = "2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21"; const std::filesystem::path payload = data / "payloads" / payload_id; - ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content")); - std::ofstream(payload / "libroblox.so") << "ELF fixture"; + WriteManagedPayloadFiles(payload); std::ofstream(data / "current.json") << "{\"schema_version\":1,\"payload_id\":\"" << payload_id << "\",\"payload_path\":\"payloads/" << payload_id @@ -182,10 +190,41 @@ TEST(RuntimePathsTest, ResolvesValidatedActivePayload) { ASSERT_TRUE(active.active); EXPECT_EQ(active.root, std::filesystem::canonical(payload)); EXPECT_EQ(active.roblox_library, active.root / "libroblox.so"); + EXPECT_EQ(active.base_apk, active.root / "sober_apk/base.apk"); + EXPECT_EQ(active.x86_64_split_apk, + active.root / "sober_apk/split_config.x86_64.apk"); EXPECT_EQ(active.assets_content, active.root / "assets/content"); EXPECT_EQ(paths.DefaultAssetPath(), active.assets_content); } +TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) { + TemporaryDirectory temporary; + ASSERT_FALSE(temporary.path().empty()); + const std::filesystem::path data = temporary.path() / "data"; + const std::string payload_id = + "2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21"; + const std::filesystem::path payload = data / "payloads" / payload_id; + WriteManagedPayloadFiles(payload); + ASSERT_TRUE(std::filesystem::remove( + payload / "sober_apk/split_config.x86_64.apk")); + std::ofstream(data / "current.json") + << "{\"schema_version\":1,\"payload_id\":\"" << payload_id + << "\",\"payload_path\":\"payloads/" << payload_id + << "\",\"version_name\":\"2.725.1142\",\"version_code\":2546," + "\"elf_build_id\":\"d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21\"}"; + const MapEnvironment environment({ + {"HOME", temporary.path().string()}, + {"MOCKTAIL_DATA_ROOT", data.string()}, + }); + + const ActivePayloadPaths active = + RuntimePaths::FromEnvironment(environment, temporary.path()) + .ResolveActivePayload(); + + EXPECT_FALSE(active); + EXPECT_NE(active.error.find("split_config.x86_64.apk"), std::string::npos); +} + TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) { TemporaryDirectory temporary; ASSERT_FALSE(temporary.path().empty()); @@ -197,11 +236,10 @@ TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) { const std::string approval_filename = payload_id + "-" + approval_generation + ".json"; const std::filesystem::path payload = data / "payloads" / payload_id; - ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content")); + WriteManagedPayloadFiles(payload); ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "compatibility_profiles")); ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "host_abi_profiles")); ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "approvals")); - std::ofstream(payload / "libroblox.so") << "ELF fixture"; std::ofstream(data / "compatibility_profiles" / approval_filename) << "{}"; std::ofstream(data / "host_abi_profiles" / approval_filename) << "{}"; std::ofstream(data / "approvals" / approval_filename) << "{}"; @@ -247,8 +285,7 @@ TEST(RuntimePathsTest, RejectsPartialOrEscapingApprovedProfileReferences) { const std::string approval_filename = payload_id + "-" + approval_generation + ".json"; const std::filesystem::path payload = data / "payloads" / payload_id; - ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content")); - std::ofstream(payload / "libroblox.so") << "ELF fixture"; + WriteManagedPayloadFiles(payload); const MapEnvironment environment({ {"HOME", temporary.path().string()}, {"MOCKTAIL_DATA_ROOT", data.string()}, @@ -468,7 +505,7 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) { ScopedCurrentPath restore_current_path; const std::filesystem::path data = temporary.path() / "data"; const std::filesystem::path payload = data / "payloads/fixture"; - ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "assets/content")); + WriteManagedPayloadFiles(payload); const MapEnvironment environment({ {"HOME", temporary.path().string()}, {"MOCKTAIL_DATA_ROOT", data.string()}, @@ -478,6 +515,10 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) { ActivePayloadPaths active; active.active = true; active.root = payload; + active.roblox_library = payload / "libroblox.so"; + active.base_apk = payload / "sober_apk/base.apk"; + active.x86_64_split_apk = + payload / "sober_apk/split_config.x86_64.apk"; active.assets_content = payload / "assets/content"; std::string error; @@ -487,6 +528,74 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) { EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/assets")); EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/assets"), std::filesystem::canonical(payload / "assets")); + EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/sober_apk")); + EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/sober_apk"), + std::filesystem::canonical(payload / "sober_apk")); + EXPECT_TRUE(std::filesystem::is_symlink(data / "rbx_bin/libroblox.so")); + EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/libroblox.so"), + std::filesystem::canonical(payload / "libroblox.so")); +} + +TEST(RuntimePathsTest, ReplacesAllStaleManagedPayloadLinksTogether) { + TemporaryDirectory temporary; + ASSERT_FALSE(temporary.path().empty()); + ScopedCurrentPath restore_current_path; + const std::filesystem::path data = temporary.path() / "data"; + const std::filesystem::path old_payload = data / "payloads/old"; + const std::filesystem::path new_payload = data / "payloads/new"; + WriteManagedPayloadFiles(old_payload); + WriteManagedPayloadFiles(new_payload); + const MapEnvironment environment({ + {"HOME", temporary.path().string()}, + {"MOCKTAIL_DATA_ROOT", data.string()}, + }); + const RuntimePaths paths = + RuntimePaths::FromEnvironment(environment, temporary.path()); + ActivePayloadPaths active; + active.active = true; + active.root = old_payload; + + std::string error; + ASSERT_TRUE(PrepareManagedPayloadWorkingDirectory(paths, active, &error)) + << error; + active.root = new_payload; + ASSERT_TRUE(PrepareManagedPayloadWorkingDirectory(paths, active, &error)) + << error; + + EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/assets"), + std::filesystem::canonical(new_payload / "assets")); + EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/sober_apk"), + std::filesystem::canonical(new_payload / "sober_apk")); + EXPECT_EQ(std::filesystem::canonical(data / "rbx_bin/libroblox.so"), + std::filesystem::canonical(new_payload / "libroblox.so")); +} + +TEST(RuntimePathsTest, PreservesUnexpectedManagedPayloadFiles) { + TemporaryDirectory temporary; + ASSERT_FALSE(temporary.path().empty()); + ScopedCurrentPath restore_current_path; + const std::filesystem::path data = temporary.path() / "data"; + const std::filesystem::path payload = data / "payloads/fixture"; + WriteManagedPayloadFiles(payload); + ASSERT_TRUE(RuntimePaths::EnsureDirectory(data / "rbx_bin/sober_apk")); + std::ofstream(data / "rbx_bin/sober_apk/user-file") << "preserve me"; + const MapEnvironment environment({ + {"HOME", temporary.path().string()}, + {"MOCKTAIL_DATA_ROOT", data.string()}, + }); + const RuntimePaths paths = + RuntimePaths::FromEnvironment(environment, temporary.path()); + ActivePayloadPaths active; + active.active = true; + active.root = payload; + + std::string error; + EXPECT_FALSE(PrepareManagedPayloadWorkingDirectory(paths, active, &error)); + EXPECT_NE(error.find("refusing to replace non-symlink"), std::string::npos); + EXPECT_TRUE(std::filesystem::is_regular_file( + data / "rbx_bin/sober_apk/user-file")); + EXPECT_FALSE(std::filesystem::exists(data / "rbx_bin/assets")); + EXPECT_FALSE(std::filesystem::exists(data / "rbx_bin/libroblox.so")); } TEST(RuntimePathsTest, ExportsXdgBackedAndroidPathsAndCreatesParents) { diff --git a/tests/system_install_test.sh b/tests/system_install_test.sh index 6c04a96..24d36b6 100644 --- a/tests/system_install_test.sh +++ b/tests/system_install_test.sh @@ -23,6 +23,9 @@ readonly icon="${install_root}/usr/share/icons/hicolor/scalable/apps/space.bigra [[ -x "${runtime}/mocktail_updater" ]] [[ -x "${runtime}/mocktail_failure_dialog" ]] [[ -x "${runtime}/mocktail_webview_helper" ]] +[[ -x "${runtime}/mocktail_freebsd_socket_helper" ]] +LC_ALL=C readelf -h "${runtime}/mocktail_freebsd_socket_helper" | + grep -Fq 'UNIX - FreeBSD' readelf -h "${binary}" | grep -Fq 'ELF64' readelf -h "${runtime}/mocktail_updater" | grep -Fq 'ELF64' readelf -d "${binary}" | grep -Fq '$ORIGIN/../lib/mocktail'