@@ -1095,6 +1095,48 @@ struct ProtocolTests {
10951095 try expect ( response. result == nil , " oversized request must not reach the command handler " )
10961096 }
10971097
1098+ static func differentPeerUserIsRejected( ) throws {
1099+ #if os(Linux)
1100+ // The Linux CI container runs this suite as root, which lets the test
1101+ // launch one deliberately unprivileged peer. Normal developer runs
1102+ // still exercise every other transport boundary without requiring
1103+ // privilege escalation.
1104+ guard getuid ( ) == 0 else { return }
1105+ let setpriv = " /usr/bin/setpriv "
1106+ try expect ( FileManager . default. isExecutableFile ( atPath: setpriv) , " Linux CI should provide setpriv " )
1107+ try LocalRuntime . preparePrivateDirectory ( )
1108+ let socketPath = LocalRuntime . directoryURL
1109+ . appendingPathComponent ( " peer-uid- \( UUID ( ) . uuidString) .sock " ) . path
1110+ let server = LocalSocketServer ( socketPath: socketPath)
1111+ try server. start { request in CommandResponse . success ( id: request. id) }
1112+ defer {
1113+ server. stop ( )
1114+ _ = Glibc . chmod ( LocalRuntime . directoryURL. path, 0o700 )
1115+ }
1116+ // The production modes are 0700/0600. Open them only inside this
1117+ // disposable test so a different uid can reach accept(), where the
1118+ // credential check must still fail closed.
1119+ try expect ( Glibc . chmod ( LocalRuntime . directoryURL. path, 0o777 ) == 0 , " test runtime directory chmod failed " )
1120+ try expect ( Glibc . chmod ( socketPath, 0o666 ) == 0 , " test socket chmod failed " )
1121+
1122+ let process = Process ( )
1123+ process. executableURL = URL ( fileURLWithPath: setpriv)
1124+ process. arguments = [
1125+ " --reuid=65534 " , " --regid=65534 " , " --clear-groups " ,
1126+ CommandLine . arguments [ 0 ] , " --peer-denied-client " , socketPath,
1127+ ]
1128+ process. standardOutput = FileHandle . nullDevice
1129+ let errors = Pipe ( )
1130+ process. standardError = errors
1131+ try process. run ( )
1132+ process. waitUntilExit ( )
1133+ let errorText = String (
1134+ decoding: errors. fileHandleForReading. readDataToEndOfFile ( ) , as: UTF8 . self
1135+ )
1136+ try expect ( process. terminationStatus == 0 , " different-uid peer was not rejected: \( errorText) " )
1137+ #endif
1138+ }
1139+
10981140 static func screenshotSeriesHelpers( ) throws {
10991141 let rawPlan = JSONValue . object ( [
11001142 " initialY " : . number( 240 ) ,
@@ -1386,6 +1428,24 @@ struct ProtocolTests {
13861428 }
13871429
13881430 static func main( ) {
1431+ if CommandLine . arguments. count == 3 ,
1432+ CommandLine . arguments [ 1 ] == " --peer-denied-client " {
1433+ do {
1434+ let descriptor = try connectRawUnixSocket ( path: CommandLine . arguments [ 2 ] )
1435+ defer { closeRawSocket ( descriptor) }
1436+ let response = try ProtocolCodec . decodeLine (
1437+ CommandResponse . self, from: readRawSocketLine ( descriptor: descriptor)
1438+ )
1439+ guard response. error? . code == " PEER_DENIED " else {
1440+ fputs ( " expected PEER_DENIED \n " , stderr)
1441+ exit ( 1 )
1442+ }
1443+ exit ( 0 )
1444+ } catch {
1445+ fputs ( " peer client failed: \( error) \n " , stderr)
1446+ exit ( 1 )
1447+ }
1448+ }
13891449 let tests : [ TestCase ] = [
13901450 ( " request round-trip " , requestRoundTrip) ,
13911451 ( " unsafe navigation schemes " , rejectsUnsafeNavigationSchemes) ,
@@ -1426,6 +1486,7 @@ struct ProtocolTests {
14261486 ( " private socket directory " , serverRejectsSocketOutsidePrivateDirectory) ,
14271487 ( " shutdown bypasses busy request " , shutdownBypassesBusyRequest) ,
14281488 ( " oversized socket request " , oversizedSocketRequestIsRejected) ,
1489+ ( " different peer uid " , differentPeerUserIsRejected) ,
14291490 ]
14301491
14311492 var failures = 0
0 commit comments