RootHide loader links for later-installed applications
Observation and boundary
The 2026-09-24 TrollSpeed crash reports an unresolved
@loader_path/.jbroot/usr/lib/libroothide.dylib. dyld tried the physical
<jbroot>/Applications/TrollSpeed.app/.jbroot/usr/lib/libroothide.dylib
and stopped before the application began executing. This identifies a loader
path failure, but the report alone does not prove whether the .jbroot link,
the target usr/lib/libroothide.dylib, or both are missing. Inspect both on
the guest before changing anything.
RootHide's developer documentation
specifies a .jbroot link in every directory containing a Mach-O image and
says package installation or the jailbreak's loader normally creates it.
Apple's dynamic library documentation
defines @loader_path relative to the referencing image. A link beside the
main executable alone will not cover a dependent image in an embedded
framework or extension directory.
Present vphone behavior
GuestIrisinInstaller.ensureRootHideLinksseeds the bootstrap root andbin,sbin,usr/bin,usr/sbin,usr/lib,usr/libexec, andusr/lib/pamat initial bootstrap installation and onvphonedstartup. The PAM modules need their own link: without it sshd reportsPAM: initialisation failed. It does not coverApplications/*.appinstalled later.- Irisin unpacks packages without RootHide dpkg's hook, so nothing linked
deeper package directories. sudo failed (#520) because
usr/libexec/sudo/libsudo_util.0.dylibcould not load@loader_path/.jbroot/usr/lib/libvrootapi.dylib;apt/methods,engines-3andossl-moduleshave the same gap. After the fixed list,ensureRootHideMachOLinkswalksbin,sbin,usr(minususr/shareandusr/include) andLibrarywithout following symlinks, reads each regular file's magic until a directory shows a thin 64-bit or fat Mach-O, and creates../×depth +.jbrootthere. A directory with any.jbrootentry is skipped, and a failed link is not fatal. It runs at install, onvphonedstartup, and one second after the root'sLibrary/dpkgchanges:watchBootstrapPackageskeeps a vnode watch on that directory, which Irisin, apt and dpkg all rewrite when a package operation finishes. - The spawn hooks cannot replace that watch.
sudois usually run from a mobile shell (an ssh session, orighostvtd-io's zsh), and mobile cannot create a link in the root-ownedusr/libexec/sudo. On 26.6.2, with the link removed and the hooks in place, sudo still failed from a mobile shell; touchingLibrary/dpkgrecreated the link andsudo idreturned root. - launchd (26.6.2) starts jobs through
posix_spawnp: xpcproxy, and each bootstrap LaunchDaemon such as sshd or ighostvtd. Its onlyposix_spawncall re-executes/sbin/launchd; app spawns reach the interposedposix_spawnthrough another image. The launchd hook interposes both. Before it did, no xpcproxy or bootstrap daemon loaded SystemHook, so an ssh session had none either. - SystemHook is chain-loaded into every child, by launchd and by SystemHook's
posix_spawn,posix_spawnpandexecve, whatever the child's environment says; only launchd re-executing itself is left alone. A process that rebuilds its child's environment, as sshd does for a session, would otherwise drop it.DISABLE_TWEAKS,_SafeModeand_MSSafeModeare honored in the child's constructor, which then skips ElleKit's TweakLoader. Only bootstrap, app, xpcproxy and camera processes are logged. - SystemHook's constructor loads
TweakLoader.dylibwithdlopen, but a requiredLC_LOAD_DYLIBis resolved by dyld before that constructor. An in-process repair there cannot recover this launch failure. SystemHook in the parent of a chained spawn could repair links before spawning instead. - vphone's
apps.installcalls IcliKit, which installs into an application container without creating RootHide loader links. The Irisin bootstrap installer and Irisin's own package installer are separate installation paths. Neither existing vphone link seeding nor a hook ondpkgalone covers every one of these paths.
Implemented boundary
The shared RootHideLoaderLinks helper runs before the final spawn in both
launchdhook-vphone and SystemHook-vphone. It accepts executable paths
inside the selected RootHide bootstrap, including /var and /private/var
spellings, and creates a relative .jbroot link in the executable's own
directory. It resolves and validates an existing link, refuses a non-link or
a link to another root, and never writes into an unrelated app container.
The spawn result is preserved if link creation fails; the hooks log the
failure. It then reads the executable's arm64 load commands: each
@loader_path/.jbroot/… LC_RPATH directory gets a link, and each
LC_LOAD_DYLIB-family dependency that resolves inside the root, directly or
through those rpaths, gets a link in its directory and is walked in turn.
sudo is the case this covers: usr/bin/sudo has the rpath
@loader_path/.jbroot/usr/libexec/sudo and loads
@rpath/libsudo_util.0.dylib. The walk is bounded for PID 1: 32 images,
16 rpaths and 512 KiB of load commands per image, with no symlink followed
out of the root. SystemHook runs the same walk on TweakLoader.dylib before
its dlopen, which covers usr/lib/ellekit. A link needs a writable
directory, so a walk in a mobile process only helps where mobile may write;
the vphoned pass above covers the rest.
The fixed bootstrap link seeding in vphoned remains necessary. A test
LaunchDaemon loaded after boot spawned without passing through either observed
interposer: a copied RootHide true executable under an unlinked app directory
exited with OS_REASON_DYLD (status 6), then exited successfully (status 0)
after its .jbroot link was created. Removing vphoned's usr/libexec seed
could therefore regress the first Irisin daemon launch. That daemon missed
both interposers because launchd started it through posix_spawnp, which the
launchd hook did not interpose then. An installer-side link
pass is useful for such jobs and for normal uninstall cleanup, but cannot
cover third-party installers by itself. A dyld path remap could
remove the filesystem links entirely, but would require an early, versioned
patch to the loader, a reliable current-root lookup in every affected process,
and tests for every RootHide load-command form. Its boot-wide reach makes it
the highest-risk option for this specific gap.
Further verification
- Check
lstat/readlinkof the exact app directory's.jbroot, confirm<jbroot>/usr/lib/libroothide.dylibexists, and inspect the executable'sLC_LOAD_DYLIBentries. Keep the original crash report. - Reproduce the missing-link launch on a clone. Confirm launchd logs the final physical executable and that the failure happens before SystemHook logs its constructor for that PID.
- With the link in place before spawn, confirm dyld opens the target library, TrollSpeed starts, and any later failure is diagnosed separately.
- Exercise a bundle with a framework or extension referencing the same
install name, a package upgrade, uninstall, safe mode, and a RootHide root
rerandomization. Confirm existing non-link
.jbrootentries are preserved.
On 2026-09-25, a copy of vphone-27.0-cloudos-26.4 booted with both updated
hooks. Its RootHide usr/lib/libroothide.dylib existed. The unlinked
Applications/Xrash.app launched and became frontmost; launchd logged
loader-link ... status=0, and the new .jbroot resolved to the selected
root. The same before/after result was observed for Applications/irisin.app.
The test LaunchDaemon described above was unloaded and removed. TrollSpeed
was not installed on this VM, so its exact package remains untested here.
The original named VM then booted with the two updated hooks. Xrash and
Irisin each launched frontmost, and each missing app-directory .jbroot
was created and resolved to the selected RootHide root.