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") } }