diff --git a/Resources/Info.plist b/Resources/Info.plist
index e001b734..6a51eb5e 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -38,6 +38,8 @@
1
InsomniaSessionCommandVersion
1
+ InsomniaBackstopCommand
+ --backstop
NSHumanReadableCopyright
Copyright © 2026 Krish Garg. MIT License.
diff --git a/SECURITY.md b/SECURITY.md
index 3bcbf397..04a4d746 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -47,8 +47,22 @@ a team id. Releases are ad-hoc signed, so both limits stay.
In releases built by the Release workflow and in builds of the current
source, the recovery LaunchAgent runs only the `backstop.sh` sealed inside the
-signed app bundle, after `codesign --verify --strict` passes against the code
-requirement pinned in its plist (the build's cdhash for an ad-hoc signature).
+signed app bundle, after strict signature and resource validation against the
+code requirement pinned in its plist (the build's cdhash for an ad-hoc
+signature). The native command uses the Security framework and directly
+executes `/bin/bash` with the sealed script path; the external fallback uses
+`codesign --verify --strict`.
+For current ad-hoc builds, launchd first enforces a `SpawnConstraint` with
+those cdhashes before starting `Insomnia --backstop`. A different binary,
+even one re-signed ad-hoc that skips the script verifier, is rejected before
+its entry point. This uses macOS launch-constraint enforcement, with System
+Integrity Protection enabled. The installer and every `arm()` require an
+affirmative `csrutil status` result before selecting native execution;
+disabled, custom, unreadable or timed-out status uses the external
+system-shell verifier, as do older bundles and requirements that cannot
+be represented entirely as cdhashes. After changing SIP, a previously
+registered native job depends on the new OS enforcement state until the
+app next arms it; changing SIP does not itself rewrite the stored plist.
An edited bundle or script is refused and logged. The app pins the code it is
running (its own designated requirement, after confirming that the bundle on
disk is still that code and passes the agent's check), so a bundle edited or
diff --git a/Sources/Insomnia/Core/BackstopCommand.swift b/Sources/Insomnia/Core/BackstopCommand.swift
new file mode 100644
index 00000000..d907b3ad
--- /dev/null
+++ b/Sources/Insomnia/Core/BackstopCommand.swift
@@ -0,0 +1,31 @@
+import Darwin
+import Foundation
+
+/// Launchd starts our signed executable so Login Items can attribute the
+/// recovery agent to Insomnia, then we replace it with the verified script.
+enum BackstopCommand {
+ static let argument = "--backstop"
+
+ static func run(_ arguments: [String], bundle: URL = Bundle.main.bundleURL) -> Int32? {
+ guard arguments.first == argument else { return nil }
+ guard arguments.count == 2, !arguments[1].isEmpty, bundle.pathExtension == "app" else {
+ fputs("usage: Insomnia --backstop (from Insomnia.app)\n", stderr)
+ return 64
+ }
+ do {
+ try CodeRequirement.verify(codeAt: bundle, satisfies: arguments[1])
+ } catch {
+ let reason = error.localizedDescription.replacingOccurrences(of: "\n", with: " ")
+ Log.error("backstop agent: \(bundle.path) does not satisfy the pinned code requirement; backstop.sh not run. Reinstall Insomnia (scripts/install.sh). \(reason)")
+ return 1
+ }
+ let command = ["/bin/bash", Paths.backstopScript(inBundle: bundle).path]
+ var argv = command.map { strdup($0) } + [nil]
+ defer { for argument in argv { free(argument) } }
+ return argv.withUnsafeMutableBufferPointer {
+ execv("/bin/bash", $0.baseAddress!)
+ perror("Insomnia backstop: exec /bin/bash")
+ return 1
+ }
+ }
+}
diff --git a/Sources/Insomnia/Core/LaunchdBackstop.swift b/Sources/Insomnia/Core/LaunchdBackstop.swift
index b3dcd605..7e426234 100644
--- a/Sources/Insomnia/Core/LaunchdBackstop.swift
+++ b/Sources/Insomnia/Core/LaunchdBackstop.swift
@@ -10,6 +10,9 @@ import Foundation
/// signature against the requirement pinned in the plist and only then
/// execs the script, so a backstop.sh edited on disk is never run by
/// launchd. Nothing executable lives in a writable support directory.
+/// Before launching the native entry point, launchd enforces SpawnConstraint
+/// against the pinned cdhashes. A replacement binary cannot bypass the check
+/// by declining to verify the script. Older bundles use the external shell.
///
/// What the app pins is the requirement of the code it is itself running
/// (CodeRequirement.pin), after checking that the bundle on disk still is
@@ -34,6 +37,7 @@ struct BackstopTarget: Sendable, Equatable {
/// Designated requirement of the bundle's signature, in requirement
/// language, as `codesign -d -r-` prints it (see CodeRequirement).
let requirement: String
+ var nativeExecutable: URL? = nil
var script: URL { Paths.backstopScript(inBundle: bundle) }
}
@@ -45,11 +49,12 @@ struct LaunchdBackstop: BackstopScheduling {
typealias BundlePinner = @Sendable (_ bundle: URL) throws -> String
static let launchctl = "/bin/launchctl"
+ static let csrutil = "/usr/bin/csrutil"
/// Seconds between backstop.sh runs while loaded. install.sh writes the same value.
static let pollInterval = 60
static let commandTimeout: TimeInterval = 15
- /// What launchd runs: `/bin/sh -c sh `.
+ /// The external verifier for bundles without a constrained native command.
/// The program verifies the bundle ($2) against the requirement ($1) with
/// codesign and execs the sealed backstop.sh only when that passes; the
/// resource seal covers the script, so an edited copy fails here. On
@@ -111,10 +116,28 @@ struct LaunchdBackstop: BackstopScheduling {
} catch {
throw BackstopError(message: "the recovery agent cannot pin \(bundle.path): \(error.localizedDescription). Reinstall with scripts/install.sh")
}
- let desired = Self.plistDictionary(label: label, target: BackstopTarget(bundle: bundle, requirement: requirement))
+ var executable = Self.nativeExecutable(in: bundle)
+ if executable != nil, CodeRequirement.launchHashes(in: requirement) != nil {
+ // Recheck each arm: launch constraints rely on SIP enforcement.
+ do {
+ let status = try await run(Self.csrutil, ["status"])
+ let output = status.stdout.hasSuffix("\n") ? String(status.stdout.dropLast()) : status.stdout
+ if !status.succeeded || !status.stderr.isEmpty || output != "System Integrity Protection status: enabled." {
+ executable = nil
+ }
+ } catch is CancellationError {
+ throw CancellationError()
+ } catch {
+ executable = nil
+ }
+ }
+ let desired = Self.plistDictionary(label: label, target: BackstopTarget(
+ bundle: bundle, requirement: requirement, nativeExecutable: executable
+ ))
// Armed when the plist the next login loads is this build's and the
// loaded job is the one it describes: the same command line, started
- // every `pollInterval` seconds. A loaded label alone may be another
+ // every `pollInterval` seconds, with the native spawn constraint
+ // present. A loaded label alone may be another
// build's job, pinning a bundle or requirement this one does not
// satisfy (install.sh can leave one loaded when it stops between its
// bootstrap and publishing the plist), or a job loaded from a plist
@@ -137,15 +160,30 @@ struct LaunchdBackstop: BackstopScheduling {
// MARK: Plist
+ static func nativeExecutable(in bundle: URL) -> URL? {
+ guard let data = try? Data(contentsOf: bundle.appendingPathComponent("Contents/Info.plist")),
+ let info = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any],
+ info["InsomniaBackstopCommand"] as? String == BackstopCommand.argument,
+ let name = info["CFBundleExecutable"] as? String,
+ !name.isEmpty, name != ".", name != "..", !name.contains("/") else { return nil }
+ return bundle.appendingPathComponent("Contents/MacOS").appendingPathComponent(name)
+ }
+
/// Pure builder, testable without launchd. Must produce exactly what
/// install.sh writes, or every arm() reloads the agent.
static func plistDictionary(label: String, target: BackstopTarget) -> [String: Any] {
- [
+ var plist: [String: Any] = [
"Label": label,
"ProgramArguments": ["/bin/sh", "-c", agentProgram, "sh", target.requirement, target.bundle.path],
"RunAtLoad": true,
"StartInterval": pollInterval,
]
+ if let executable = target.nativeExecutable, let hashes = CodeRequirement.launchHashes(in: target.requirement) {
+ plist["ProgramArguments"] = [executable.path, BackstopCommand.argument, target.requirement]
+ plist["SpawnConstraint"] = ["cdhash": ["$in": hashes]]
+ plist["AssociatedBundleIdentifiers"] = [Paths.bundleIdentifier]
+ }
+ return plist
}
func plistOnDiskMatches(_ desired: [String: Any]) -> Bool {
@@ -225,15 +263,19 @@ struct LaunchdBackstop: BackstopScheduling {
/// StartInterval in seconds; nil when the job has none, so launchd
/// never starts it again by itself.
var runInterval: Int?
+ /// Older native agents ran the same command without enforcing its pin.
+ var hasSpawnConstraint: Bool
- init(arguments: [String], runInterval: Int?) {
+ init(arguments: [String], runInterval: Int?, hasSpawnConstraint: Bool = false) {
self.arguments = arguments
self.runInterval = runInterval
+ self.hasSpawnConstraint = hasSpawnConstraint
}
/// The job a plist from `plistDictionary` loads as.
init(plist: [String: Any]) {
- self.init(arguments: plist["ProgramArguments"] as? [String] ?? [], runInterval: plist["StartInterval"] as? Int)
+ self.init(arguments: plist["ProgramArguments"] as? [String] ?? [], runInterval: plist["StartInterval"] as? Int,
+ hasSpawnConstraint: plist["SpawnConstraint"] != nil)
}
}
@@ -250,12 +292,14 @@ struct LaunchdBackstop: BackstopScheduling {
/// seconds` is there only for a job with a StartInterval. nil when the
/// arguments block is missing, repeated, unclosed or holds a line this
/// does not know; a run interval in another form reads as none. Either
- /// way the job does not match a plist, and arm() reloads it.
+ /// way the job does not match a plist, and arm() reloads it. The top-level
+ /// properties line reports whether the native spawn constraint is loaded.
static func loadedJob(fromPrint output: String) -> LoadedJob? {
let intervalPrefix = "\trun interval = ", intervalSuffix = " seconds"
var arguments: [String]?
var inArguments = false
var runInterval: Int?
+ var hasSpawnConstraint = false
for line in output.split(separator: "\n", omittingEmptySubsequences: false) {
if inArguments {
if line == "\t}" {
@@ -271,10 +315,13 @@ struct LaunchdBackstop: BackstopScheduling {
inArguments = true
} else if line.hasPrefix(intervalPrefix), line.hasSuffix(intervalSuffix) {
runInterval = Int(line.dropFirst(intervalPrefix.count).dropLast(intervalSuffix.count))
+ } else if line.hasPrefix("\tproperties = ") {
+ hasSpawnConstraint = line.dropFirst("\tproperties = ".count)
+ .components(separatedBy: " | ").contains("has spawn constraint")
}
}
guard let arguments, !inArguments else { return nil }
- return LoadedJob(arguments: arguments, runInterval: runInterval)
+ return LoadedJob(arguments: arguments, runInterval: runInterval, hasSpawnConstraint: hasSpawnConstraint)
}
/// bootout by service target (ignored if not loaded: the trusted path may
diff --git a/Sources/Insomnia/System/CodeRequirement.swift b/Sources/Insomnia/System/CodeRequirement.swift
index 2574b16d..713c6a46 100644
--- a/Sources/Insomnia/System/CodeRequirement.swift
+++ b/Sources/Insomnia/System/CodeRequirement.swift
@@ -16,6 +16,26 @@ import Security
/// which install.sh reads; the two must agree for the app to recognise the
/// plist install.sh wrote (PackagingTests checks this on a scratch bundle).
enum CodeRequirement {
+ /// Only a requirement made entirely of cdhash clauses can be enforced
+ /// before executing ad-hoc code. Keep the external shell verifier for
+ /// any other requirement instead of dropping a clause we do not know.
+ static func launchHashes(in requirement: String) -> [Data]? {
+ var hashes: [Data] = []
+ for clause in requirement.components(separatedBy: " or ") {
+ guard clause.hasPrefix("cdhash H\""), clause.hasSuffix("\"") else { return nil }
+ let hex = Array(clause.dropFirst(9).dropLast().utf8)
+ guard hex.count == 40, hex.allSatisfy({ "0123456789abcdefABCDEF".utf8.contains($0) }) else { return nil }
+ var hash = Data()
+ for offset in stride(from: 0, to: hex.count, by: 2) {
+ let pair = String(decoding: hex[offset..<(offset + 2)], as: UTF8.self)
+ guard let byte = UInt8(pair, radix: 16) else { return nil }
+ hash.append(byte)
+ }
+ hashes.append(hash)
+ }
+ return hashes
+ }
+
struct ReadError: LocalizedError {
let path: String
let step: String
diff --git a/Sources/Insomnia/main.swift b/Sources/Insomnia/main.swift
index 490cfb0e..e0c578c2 100644
--- a/Sources/Insomnia/main.swift
+++ b/Sources/Insomnia/main.swift
@@ -1,10 +1,15 @@
import Foundation
-// Entry point. A one-shot command-line mode is answered here, before AppKit
-// or SwiftUI start: backstop.sh borrows the app's kernel-level identity
-// check (see ResumeFrozenCommand), which ends itself when its lifetime is
-// up, and scripts drive the running app's session (see SessionCommandLine).
-// Anything else starts the menu bar app.
+// Entry point. One-shot command-line modes are answered here, before AppKit
+// or SwiftUI start: the recovery agent's backstop mode execs the verified
+// script (see BackstopCommand), backstop.sh borrows the app's kernel-level
+// identity check (see ResumeFrozenCommand), which ends itself when its
+// lifetime is up, and scripts drive the running app's session (see
+// SessionCommandLine). Anything else starts the menu bar app.
+if let status = BackstopCommand.run(Array(CommandLine.arguments.dropFirst())) {
+ exit(status)
+}
+
if let output = ResumeFrozenCommand.run(Array(CommandLine.arguments.dropFirst()), endAfter: ResumeFrozenCommand.endProcess) {
for line in output.lines { print(line) }
exit(output.status)
diff --git a/Tests/InsomniaTests/LaunchdBackstopTests.swift b/Tests/InsomniaTests/LaunchdBackstopTests.swift
index 0f3235cf..b1d1b1c0 100644
--- a/Tests/InsomniaTests/LaunchdBackstopTests.swift
+++ b/Tests/InsomniaTests/LaunchdBackstopTests.swift
@@ -10,8 +10,12 @@ final class LaunchdBackstopTests: XCTestCase {
/// fake reader below hands it out for the fixture bundle.
static let requirement = "cdhash H\"0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c\""
var home: TempHome!
- /// (exe, args) of every command the backstop ran.
+ /// (exe, args) of every launchctl command the backstop ran.
let calls = Locked<[[String]]>([])
+ let integrityStatus = Locked(ShellResult(status: 0, stdout: "System Integrity Protection status: enabled.\n", stderr: ""))
+ let integrityReadFails = Locked(false)
+ let integrityReadCancelled = Locked(false)
+ let integrityReads = Locked(0)
/// Whether `launchctl print` reports the job loaded.
let loaded = Locked(false)
/// The ProgramArguments of the plist the loaded job was bootstrapped
@@ -19,6 +23,8 @@ final class LaunchdBackstopTests: XCTestCase {
let loadedJob = Locked<[String]>([])
/// Its StartInterval, which `launchctl print` lists as `run interval`.
let loadedInterval = Locked(nil)
+ let loadedConstraint = Locked(false)
+ let printedJob = Locked(nil)
let bootstrapFails = Locked(false)
/// bootout returns an error and leaves the job loaded, as launchd does
/// for a job that is mid-transition.
@@ -34,9 +40,15 @@ final class LaunchdBackstopTests: XCTestCase {
override func setUp() {
home = TempHome()
calls.value = []
+ integrityStatus.value = ShellResult(status: 0, stdout: "System Integrity Protection status: enabled.\n", stderr: "")
+ integrityReadFails.value = false
+ integrityReadCancelled.value = false
+ integrityReads.value = 0
loaded.value = false
loadedJob.value = []
loadedInterval.value = nil
+ loadedConstraint.value = false
+ printedJob.value = nil
bootstrapFails.value = false
bootoutFails.value = false
onBootout.value = nil
@@ -56,9 +68,14 @@ final class LaunchdBackstopTests: XCTestCase {
let script = home.paths.backstopScript
try FileManager.default.createDirectory(at: script.deletingLastPathComponent(), withIntermediateDirectories: true)
try Data("#!/bin/bash\n".utf8).write(to: script)
+ let info: [String: Any] = ["InsomniaBackstopCommand": BackstopCommand.argument, "CFBundleExecutable": "Insomnia"]
+ try PropertyListSerialization.data(fromPropertyList: info, format: .xml, options: 0)
+ .write(to: home.paths.appBundle.appendingPathComponent("Contents/Info.plist"))
}
- let calls = calls, loaded = loaded, loadedJob = loadedJob, loadedInterval = loadedInterval, bootstrapFails = bootstrapFails, bootoutFails = bootoutFails
+ let calls = calls, loaded = loaded, loadedJob = loadedJob, loadedInterval = loadedInterval, loadedConstraint = loadedConstraint, bootstrapFails = bootstrapFails, bootoutFails = bootoutFails
+ let integrityStatus = integrityStatus, integrityReadFails = integrityReadFails, integrityReadCancelled = integrityReadCancelled, integrityReads = integrityReads
let onBootout = onBootout, onBootstrapped = onBootstrapped, trustedPlistAtBootstrap = trustedPlistAtBootstrap
+ let printedJob = printedJob
let trusted = home.paths.backstopPlist
let label = "com.insomnia.backstop"
// What launchd itself enforces on a path argument (proven against a
@@ -85,11 +102,18 @@ final class LaunchdBackstopTests: XCTestCase {
return requirement
}
return LaunchdBackstop(paths: home.paths, bundle: bundle, pin: pin, uid: 501) { exe, args in
+ if exe == LaunchdBackstop.csrutil {
+ integrityReads.value += 1
+ XCTAssertEqual(args, ["status"])
+ if integrityReadCancelled.value { throw CancellationError() }
+ if integrityReadFails.value { throw BackstopError(message: "status unavailable") }
+ return integrityStatus.value
+ }
calls.value.append([exe] + args)
switch args.first {
case "print":
guard loaded.value else { return ShellResult(status: 113, stdout: "", stderr: "Could not find service") }
- return ShellResult(status: 0, stdout: Self.printOutput(arguments: loadedJob.value, runInterval: loadedInterval.value), stderr: "")
+ return ShellResult(status: 0, stdout: printedJob.value ?? Self.printOutput(arguments: loadedJob.value, runInterval: loadedInterval.value, hasSpawnConstraint: loadedConstraint.value), stderr: "")
case "bootstrap":
trustedPlistAtBootstrap.value.append(try? Data(contentsOf: trusted))
guard args.count == 3, launchdAccepts(args[2]) else {
@@ -103,6 +127,7 @@ final class LaunchdBackstopTests: XCTestCase {
.flatMap { try? PropertyListSerialization.propertyList(from: $0, format: nil) as? [String: Any] }
loadedJob.value = plist?["ProgramArguments"] as? [String] ?? []
loadedInterval.value = plist?["StartInterval"] as? Int
+ loadedConstraint.value = plist?["SpawnConstraint"] != nil
if let hook = onBootstrapped.value { onBootstrapped.value = nil; hook() }
return ShellResult(status: 0, stdout: "", stderr: "")
case "bootout":
@@ -124,11 +149,77 @@ final class LaunchdBackstopTests: XCTestCase {
}
}
+ /// Captured from a disposable native job using the generated constraint.
+ /// Paths, identifiers, hashes and numeric IDs are replaced; layout and
+ /// properties are launchctl's actual output, not inferred from the plist.
+ static let capturedNative = """
+ gui/502/com.insomnia.spawn-probe.fixture = {
+ \tactive count = 0
+ \tpath = /private/tmp/insomnia-spawn-probe-fixture/com.insomnia.spawn-probe.fixture.plist
+ \ttype = LaunchAgent
+ \tstate = not running
+
+ \tprogram = /private/tmp/insomnia-spawn-probe-fixture/Insomnia Probe.app/Contents/MacOS/Insomnia
+ \targuments = {
+ \t\t/private/tmp/insomnia-spawn-probe-fixture/Insomnia Probe.app/Contents/MacOS/Insomnia
+ \t\t--backstop
+ \t\tcdhash H"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ \t}
+
+ \tinherited environment = {
+ \t\tSSH_AUTH_SOCK => /private/tmp/com.apple.launchd.fixture/Listeners
+ \t}
+
+ \tdefault environment = {
+ \t\tPATH => /usr/bin:/bin:/usr/sbin:/sbin
+ \t}
+
+ \tenvironment = {
+ \t\tOSLogRateLimit => 64
+ \t\tXPC_SERVICE_NAME => com.insomnia.spawn-probe.fixture
+ \t}
+
+ \tdomain = gui/502 [100000]
+ \tasid = 100000
+ \tminimum runtime = 10
+ \texit timeout = 5
+ \truns = 1
+ \tlast exit code = 0
+
+ \tresource coalition = {
+ \t\tID = 100
+ \t\ttype = resource
+ \t\tstate = active
+ \t\tactive count = 1
+ \t\tname = com.insomnia.spawn-probe.fixture
+ \t}
+
+ \tjetsam coalition = {
+ \t\tID = 101
+ \t\ttype = jetsam
+ \t\tstate = active
+ \t\tactive count = 1
+ \t\tname = com.insomnia.spawn-probe.fixture
+ \t}
+
+ \tspawn type = daemon (3)
+ \tjetsam priority = 40
+ \tjetsam memory limit (active) = (unlimited)
+ \tjetsam memory limit (inactive) = (unlimited)
+ \tjetsamproperties category = daemon
+ \tjetsam thread limit = 32
+ \tcpumon = default
+ \trun interval = 60 seconds
+
+ \tproperties = runatload | inferred program | has spawn constraint
+ }
+ """
+
/// `launchctl print gui/501/