Files
LakrandClaude Opus 5.5 9d3551cfa2 Load the bootstrap's LaunchDaemons from vphoned, as RootHide does
The launchd hook used to add the bootstrap's Library/LaunchDaemons to
launchd's cache under /System/Library/LaunchDaemons/vphone.<name> keys. A
job imported that way does not match the plist path a package script boots
out, so upgrades could not unload their own daemons. RootHide's own hook
loads only basebin/LaunchDaemons and leaves the rest to `jbctl startup`.

The hook no longer interposes xpc_dictionary_get_value. After boot vphoned
loads each plist in <root>/Library/LaunchDaemons under its real path. For
RootHide it first rewrites the plist on disk the way RootHide's launchctl
does (plistpatch.m, ported to Swift): Program, ProgramArguments[0], the
working and root directories, standard streams, watch and queue paths,
HOME/TMPDIR, KeepAlive.PathState, socket paths and fsevents paths get the
root prepended, and __Patched marks the plist. services.load patches a
plist under the root the same way, so no launchctl binary is needed.

vphoned installs RootHide under one fixed name,
.jbroot-000114514191980C, and the hook looks only there and in /var/jb.
A RootHide root under another name must be uninstalled first.

Tested on a RootHide guest (iOS 26.6.2): every daemon plist carries
__Patched and is registered under its own path, ssh and sudo work, and
launchctl bootout/bootstrap of ighostvtd and a dpkg install, remove and
reinstall of openssh-server all load the job again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:43:59 +09:00

74 lines
3.1 KiB
Swift

import Foundation
import IcliKit
import IcliSystem
// MARK: - launchd Services
extension GuestAPI {
static func executeService(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
switch method {
case "services.list":
var result = try listServices()
if let disabled = try? disabledServiceOverrides()["disabled"] as? [String: Bool] {
let rows = result["services"] as? [[String: Any]] ?? []
result["services"] = rows.map { row -> [String: Any] in
var service = row
if let label = row["label"] as? String, let isDisabled = disabled[label] {
service["disabled"] = isDisabled
}
return service
}
}
return result
case "services.status":
return try serviceStatus(string(params, "label"))
case "services.print":
return try printService(string(params, "label"))
case "services.dump":
return try servicesDump()
case "services.disabled":
return try disabledServiceOverrides()
case "services.start":
return try startService(string(params, "label"))
case "services.enable":
return try setServiceEnabled(string(params, "label"), enabled: true)
case "services.stop":
let label = try string(params, "label")
try requireForce(params, "stop \(label)")
return try stopService(label)
case "services.disable":
let label = try string(params, "label")
try requireForce(params, "disable \(label)")
return try setServiceEnabled(label, enabled: false)
case "services.remove":
let label = try string(params, "label")
try requireForce(params, "remove \(label)")
return try removeService(label)
case "services.signal":
let label = try string(params, "label")
let signal = try string(params, "signal")
try requireForce(params, "send \(signal) to \(label)")
return try signalService(label, signal: signal)
case "services.load", "services.unload":
guard let paths = params["paths"] as? [String], !paths.isEmpty else {
throw GuestAPIError.invalidRequest("paths must list plist files or directories")
}
let load = method == "services.load"
if load {
try GuestIrisinInstaller.prepareBootstrapDaemons(paths)
} else {
try requireForce(params, "unload services")
}
return try loadServices(paths, load: load, override: bool(params, "override"))
case "launchd.getenv":
return try launchdEnvironment(string(params, "key"))
case "launchd.setenv":
return try setLaunchdEnvironment(string(params, "key"), value: string(params, "value"))
case "launchd.unsetenv":
return try setLaunchdEnvironment(string(params, "key"), value: nil)
default:
return nil
}
}
}