Files
reyna-cli/native/ReynaCLIHost/Tests/ReynaCLIHostTests/SocketPathValidationTests.swift
T
Adolfo Reyna 9fd04b0ce4 feat(privacy-host): add signed native calendar contacts and reminders host
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
2026-08-03 20:27:54 -04:00

333 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
import Foundation
import Darwin
@testable import ReynaCLIHostCore
final class SocketPathValidationTests: XCTestCase {
// Helpers to build fake LStatInfo
private func dir(uid: uid_t, mode: mode_t, symlink: Bool = false) -> LStatInfo {
return LStatInfo(uid: uid, mode: mode_t(mode), isSymlink: symlink, isDir: !symlink, exists: true)
}
private func currentUID() -> uid_t { getuid() }
// Real default socket path: $HOME/Library/Application Support/reyna-cli/privacy/reyna-cli.sock
// Actual Mac modes from bug report:
// home /Users/adolforeyna = 0750, ~/Library = 0700, ~/Library/Application Support = 0700, privacy = 0700
// Tier 2 allows 0750 for intermediates, tier 3 requires 0700 for dedicated runtime parent.
func testRealDefaultHomeSocketPathValidationAccepts() throws {
let uid = currentUID()
let home = NSHomeDirectory() // /Users/adolforeyna
let socketPath = (home as NSString).appendingPathComponent("Library/Application Support/reyna-cli/privacy/reyna-cli.sock")
var map: [String: LStatInfo] = [:]
map["/"] = dir(uid: 0, mode: 0o40755)
map["/Users"] = dir(uid: 0, mode: 0o40755)
// helper to insert chain
func insertChain(upTo target: String, defaultMode: mode_t, overrides: [String: mode_t] = [:]) {
let url = URL(fileURLWithPath: target)
var cur = ""
for comp in url.pathComponents {
if comp == "/" { cur = "/"; continue }
if cur == "/" { cur = "/" + comp } else if cur.isEmpty { cur = comp } else { cur = cur + "/" + comp }
if map[cur] != nil { continue }
if cur == "/" || cur == "/Users" { continue }
let m = overrides[cur] ?? defaultMode
map[cur] = dir(uid: uid, mode: m)
}
}
// Home itself 0750 per actual system
map[home] = dir(uid: uid, mode: 0o40750)
// Library and subdirs 0700 except home already set
// Build full parent chain to privacy
let parentOfSocket = URL(fileURLWithPath: socketPath).deletingLastPathComponent().path
// For parent chain: home is 0750 override, others 0700
var chainCur = ""
for comp in URL(fileURLWithPath: parentOfSocket).pathComponents {
if comp == "/" { chainCur = "/"; continue }
if chainCur == "/" { chainCur = "/" + comp } else if chainCur.isEmpty { chainCur = comp } else { chainCur = chainCur + "/" + comp }
if map[chainCur] != nil { continue }
if chainCur == "/" || chainCur == "/Users" { continue }
if chainCur == home { continue } // already 0750
map[chainCur] = dir(uid: uid, mode: 0o40700)
}
let provider: LStatProvider = { path in map[path] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Real default home with 0750 home and 0700 Library/.../privacy must validate")
XCTAssertTrue(platformTrustedRootPaths.contains("/Users"))
XCTAssertTrue(platformTrustedRootPaths.contains("/"))
}
func testRealDefaultHomeSocketPathValidationAcceptsWithHome0755() throws {
// Also allow 0755 for home (some configs) – tier 2 should still allow as no write
let uid = currentUID()
let home = NSHomeDirectory()
let socketPath = (home as NSString).appendingPathComponent("Library/Application Support/reyna-cli/privacy/reyna-cli.sock")
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755)
]
map[home] = dir(uid: uid, mode: 0o40755)
let parentPath = URL(fileURLWithPath: socketPath).deletingLastPathComponent().path
var cur = ""
for comp in URL(fileURLWithPath: parentPath).pathComponents {
if comp == "/" { cur = "/"; continue }
if cur == "/" { cur = "/" + comp } else if cur.isEmpty { cur = comp } else { cur = cur + "/" + comp }
if map[cur] != nil { continue }
if cur == "/" || cur == "/Users" { continue }
map[cur] = dir(uid: uid, mode: 0o40700)
}
let provider: LStatProvider = { map[$0] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Home 0755 should also be allowed (no write)")
}
// Safe vs unsafe ancestor decision with fake stats
func testTrustedRootMustBeRootOwned() {
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: uid, mode: 0o40755), // wrong: owned by current user, should fail - /Users must be uid 0
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider)) { err in
let msg = (err as NSError).localizedDescription
XCTAssertTrue(msg.contains("/Users") || msg.contains("current uid") || msg.contains("not owned"))
}
}
func testTrustedRootMustNotBeWorldWritable() {
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40777), // world writable unsafe
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider)) { err in
XCTAssertTrue((err as NSError).code == 25 || (err as NSError).localizedDescription.contains("permissions"))
}
}
func testRejectsArbitraryRootOwnedIntermediatePath() {
// E.g. /tmp/root_owned_dir owned by root should be REJECTED because not in allowlist
let uid = currentUID()
let socketPath = "/tmp/root_owned_dir/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
// /tmp is symlink-allowed platform path
"/tmp": dir(uid: 0, mode: 0o120777, symlink: true), // symlink allowed
"/tmp/root_owned_dir": dir(uid: 0, mode: 0o40700) // root owned but not allowlisted
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Arbitrary root-owned path /tmp/root_owned_dir must be rejected – only explicit allowlist trusted")
}
func testRejectsHomeNotOwnedByCurrentUID() {
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: 0, mode: 0o40700) // wrong owner
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider))
}
func testAllowsHome0750ForIntermediateButRejectsWritable() {
// Tier 2: intermediate ancestors allow 0750/0755, reject writable bits
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/Application Support/reyna-cli/privacy/reyna.sock"
// 0750 for home should be accepted (intermediate)
var mapAllow: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40750),
"/Users/\(NSUserName())/Library": dir(uid: uid, mode: 0o40700),
"/Users/\(NSUserName())/Library/Application Support": dir(uid: uid, mode: 0o40700),
"/Users/\(NSUserName())/Library/Application Support/reyna-cli": dir(uid: uid, mode: 0o40700),
"/Users/\(NSUserName())/Library/Application Support/reyna-cli/privacy": dir(uid: uid, mode: 0o40700)
]
let providerAllow: LStatProvider = { mapAllow[$0] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: providerAllow),
"HOME 0750 as intermediate must be allowed (no write bits)")
// 0770 (group writable) for home must be rejected
var mapReject: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40770)
]
let socketPath2 = "/Users/\(NSUserName())/Library/reyna.sock"
let providerReject: LStatProvider = { mapReject[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath2, currentUID: uid, provider: providerReject),
"HOME 0770 (group writable) must be rejected even for intermediate")
// 0777 world writable must be rejected
var mapReject2: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40777)
]
let providerReject2: LStatProvider = { mapReject2[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath2, currentUID: uid, provider: providerReject2),
"HOME 0777 must be rejected")
}
func testRejectsDedicatedRuntimeParentWith0750or0755() {
let uid = currentUID()
let home = NSHomeDirectory()
let socketPath = (home as NSString).appendingPathComponent("Library/Application Support/reyna-cli/privacy/reyna-cli.sock")
// privacy dir 0750 must be rejected (tier 3 requires 0700)
var map0750: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
]
// build chain but make privacy 0750
var cur = ""
for comp in URL(fileURLWithPath: URL(fileURLWithPath: socketPath).deletingLastPathComponent().path).pathComponents {
if comp == "/" { cur = "/"; continue }
if cur == "/" { cur = "/" + comp } else if cur.isEmpty { cur = comp } else { cur = cur + "/" + comp }
if map0750[cur] != nil { continue }
if cur == "/" || cur == "/Users" { continue }
if cur.hasSuffix("/privacy") {
map0750[cur] = dir(uid: uid, mode: 0o40750)
} else if cur == home {
map0750[cur] = dir(uid: uid, mode: 0o40750) // home 0750 allowed
} else {
map0750[cur] = dir(uid: uid, mode: 0o40700)
}
}
let provider0750: LStatProvider = { map0750[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider0750),
"Dedicated runtime parent privacy with 0750 must be rejected – requires 0700")
// 0755 also rejected
var map0755 = map0750
let privacyPath = URL(fileURLWithPath: socketPath).deletingLastPathComponent().path
map0755[privacyPath] = dir(uid: uid, mode: 0o40755)
let provider0755: LStatProvider = { map0755[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider0755),
"Dedicated runtime parent privacy with 0755 must be rejected")
}
func testAllowsIntermediate0755ButRequiresPrivacy0700() {
let uid = currentUID()
let home = NSHomeDirectory()
let socketPath = (home as NSString).appendingPathComponent("Library/Application Support/reyna-cli/privacy/reyna-cli.sock")
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
]
var cur = ""
for comp in URL(fileURLWithPath: URL(fileURLWithPath: socketPath).deletingLastPathComponent().path).pathComponents {
if comp == "/" { cur = "/"; continue }
if cur == "/" { cur = "/" + comp } else if cur.isEmpty { cur = comp } else { cur = cur + "/" + comp }
if map[cur] != nil { continue }
if cur == "/" || cur == "/Users" { continue }
if cur == home {
map[cur] = dir(uid: uid, mode: 0o40755) // home 0755 allowed as intermediate
} else if cur.hasSuffix("/privacy") == false {
// intermediate Library etc can be 0750/0755
map[cur] = dir(uid: uid, mode: 0o40750)
} else {
map[cur] = dir(uid: uid, mode: 0o40700) // privacy must be 0700
}
}
let provider: LStatProvider = { map[$0] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Intermediate 0750/0755 allowed, privacy 0700 must validate")
}
func testRejectsSymlinkInUserChain() {
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40700),
"/Users/\(NSUserName())/Library": dir(uid: uid, mode: 0o120777, symlink: true)
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Symlink in user chain must be rejected")
}
func testAllowsPlatformSymlinksTmpVar() {
let uid = currentUID()
let socketPath = "/tmp/rh-test/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/private": dir(uid: 0, mode: 0o40755),
"/private/tmp": dir(uid: 0, mode: 0o41777), // /private/tmp typically 1777
"/tmp": dir(uid: 0, mode: 0o120777, symlink: true), // allowed symlink
"/tmp/rh-test": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider))
}
func testRejectsDotDot() {
let uid = currentUID()
let socketPath = "/tmp/rh/../evil.sock"
var map: [String: LStatInfo] = [:]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider))
}
func testNoBroadGlobalShortcutOnlyAllowlist() {
// Ensure implementation does not accept every root-owned path.
// For path /opt/rootdir where /opt is root-owned (simulating arbitrary root path), it must be rejected unless explicitly allowlisted.
let uid = currentUID()
let socketPath = "/opt/rootdir/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/opt": dir(uid: 0, mode: 0o40755), // root owned, not allowlisted
"/opt/rootdir": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Must not accept arbitrary root-owned /opt")
}
func testAllowsPrivateVarChain() {
// macOS: /var -> /private/var, /private/var/tmp etc are platform trusted
let uid = currentUID()
let socketPath = "/private/var/tmp/rh-test/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/private": dir(uid: 0, mode: 0o40755),
"/private/var": dir(uid: 0, mode: 0o40755),
"/private/var/tmp": dir(uid: 0, mode: 0o41777),
"/private/var/tmp/rh-test": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertNoThrow(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider))
}
func testRejectsGroupWritableIntermediate() {
let uid = currentUID()
let socketPath = "/Users/\(NSUserName())/Library/Application Support/reyna.sock"
var map: [String: LStatInfo] = [
"/": dir(uid: 0, mode: 0o40755),
"/Users": dir(uid: 0, mode: 0o40755),
"/Users/\(NSUserName())": dir(uid: uid, mode: 0o40755),
"/Users/\(NSUserName())/Library": dir(uid: uid, mode: 0o40770),
"/Users/\(NSUserName())/Library/Application Support": dir(uid: uid, mode: 0o40700)
]
let provider: LStatProvider = { map[$0] }
XCTAssertThrowsError(try validateParentChainPure(socketPath: socketPath, currentUID: uid, provider: provider),
"Intermediate Library 0770 group writable must be rejected")
}
}