import XCTest import Foundation import Darwin @testable import ReynaCLIHostCore final class LStatFailClosedTests: XCTestCase { 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 file(uid: uid_t, mode: mode_t) -> LStatInfo { // regular file: not symlink, not dir return LStatInfo(uid: uid, mode: mode_t(mode), isSymlink: false, isDir: false, exists: true) } private func currentUID() -> uid_t { getuid() } // RED: EACCES and ELOOP must not be treated as missing (fail-closed) func testNonENOENTProviderFailureRejectsWithCode22() { let uid = currentUID() let socketPath = "/tmp/rhfail/reyna.sock" // Map only root trusted; but intermediate component will fail with EACCES var mapPresent: [String: LStatInfo] = [ "/": dir(uid: 0, mode: 0o40755), "/private": dir(uid: 0, mode: 0o40755), "/private/tmp": dir(uid: 0, mode: 0o41777), "/tmp": dir(uid: 0, mode: 0o120777, symlink: true) ] let provider: LStatResultProvider = { p in if p == "/tmp/rhfail" { return .failed(errnoCode: EACCES) } if let v = mapPresent[p] { return .present(v) } return .absent } XCTAssertThrowsError(try validateParentChainPureResultProvider(socketPath: socketPath, currentUID: uid, provider: provider)) { err in XCTAssertEqual((err as NSError).code, 22, "EACCES must surface as lstat failure code 22, not absent") } let providerLoop: LStatResultProvider = { p in if p == "/tmp/rhfail" { return .failed(errnoCode: ELOOP) } if let v = mapPresent[p] { return .present(v) } return .absent } XCTAssertThrowsError(try validateParentChainPureResultProvider(socketPath: socketPath, currentUID: uid, provider: providerLoop)) { err in XCTAssertEqual((err as NSError).code, 22, "ELOOP must surface as code 22, not treated as ENOENT") } } func testEAccesAnywhereInChainRejects() { let uid = currentUID() let socketPath = "/Users/\(NSUserName())/Library/reyna.sock" let provider: LStatResultProvider = { p in if p == "/Users" { return .failed(errnoCode: EACCES) } return .absent } XCTAssertThrowsError(try validateParentChainPureResultProvider(socketPath: socketPath, currentUID: uid, provider: provider)) } // RED: regular file at /tmp or /var must be rejected (old ensureParentDirectories had bug where it skipped directory check for those) func testRegularFileAtTmpMustReject() throws { let uid = currentUID() let socketPath = "/tmp/reyna.sock" // /tmp exists as regular file (not dir, not symlink) var map: [String: LStatInfo] = [ "/": dir(uid: 0, mode: 0o40755), "/tmp": file(uid: 0, mode: 0o100644) // regular file ] let provider: LStatResultProvider = { p in if let v = map[p] { return .present(v) } return .absent } XCTAssertThrowsError(try validateParentChainPureResultProvider(socketPath: socketPath, currentUID: uid, provider: provider), "Regular file at /tmp must reject (not directory)") // Also test direct single-component validator XCTAssertThrowsError(try validateSingleLStatInfoOrThrow(path: "/tmp", info: map["/tmp"]!, currentUID: uid)) XCTAssertThrowsError(try validateSingleLStatInfoOrThrow(path: "/var", info: file(uid: 0, mode: 0o100644), currentUID: uid)) } func testRegularFileAtIntermediateTrustedAliasMustReject() { let uid = currentUID() // /private/var exists as file var map: [String: LStatInfo] = [ "/": dir(uid: 0, mode: 0o40755), "/private": dir(uid: 0, mode: 0o40755), "/private/var": file(uid: 0, mode: 0o100644) ] let provider: LStatResultProvider = { p in if let v = map[p] { return .present(v) } return .absent } XCTAssertThrowsError(try validateParentChainPureResultProvider(socketPath: "/private/var/tmp/x/reyna.sock", currentUID: uid, provider: provider)) } func testLiveProviderDoesNotSwallowNonENOENT() { // liveLStatProvider legacy should now fail-closed sentinel, not nil // Simulate by calling wrapper directly: we can't easily force EACCES without real FS, // but we can assert that failed case in result provider is distinct from absent let absent = LStatResult.absent let failed = LStatResult.failed(errnoCode: EACCES) switch absent { case .absent: break default: XCTFail() } switch failed { case .failed(let c): XCTAssertEqual(c, EACCES) default: XCTFail() } // legacy provider should return non-nil sentinel for failed case (so caller doesn't treat as missing) // We test sentinel is non-nil and will be rejected by validator // The new liveLStatResultProvider is tested via chain above } func testTmpSymlinkStillAllowed() throws { let uid = currentUID() let map: [String: LStatInfo] = [ "/": dir(uid: 0, mode: 0o40755), "/private": dir(uid: 0, mode: 0o40755), "/private/tmp": dir(uid: 0, mode: 0o41777), "/tmp": dir(uid: 0, mode: 0o120777, symlink: true), "/tmp/rh-test": dir(uid: uid, mode: 0o40700) ] let provider: LStatResultProvider = { p in if let v = map[p] { return .present(v) } return .absent } XCTAssertNoThrow(try validateParentChainPureResultProvider(socketPath: "/tmp/rh-test/reyna.sock", currentUID: uid, provider: provider)) } }