Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.11.0
3.0.0
235 changes: 229 additions & 6 deletions Makefile

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Website/public/ipsw.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
</dict>
<dict>
<key>ProductVersion</key>
<string>26.3.2</string>
<string>26.4.1</string>
<key>BuildVersion</key>
<string>25D2140</string>
<string>25E253</string>
<key>FirmwareURL</key>
<string>https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-94879/40A2B65E-4E49-4EAA-8BEC-62A305007488/UniversalMac_26.3.2_25D2140_Restore.ipsw</string>
<string>https://updates.cdn-apple.com/2026WinterFCS/fullrestores/122-28781/DCB2FF13-06CB-44C2-BCA2-DFCAF3521D46/UniversalMac_26.4.1_25E253_Restore.ipsw</string>
<key>FirmwareSHA1</key>
<string>338037f50ea1d6b39e7e9f82a5276e8b1f693ee8</string>
<string>03078f4af82bff5473398ca49f99288c76253fe8</string>
</dict>
</array>
</dict>
Expand Down
288 changes: 288 additions & 0 deletions experiments/kqueue-vsock-test/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
#!/usr/bin/env swift
//
// kqueue-vsock-test: Does kqueue fire for AF_VSOCK on macOS?
//
// Run inside a macOS guest VM. Listens on vsock port 9999,
// then tests whether kqueue/poll/DispatchSource detect readability
// on the accepted connection.
//
// Usage:
// 1. Build & run in the guest: swift main.swift
// 2. From the host, connect: vmctl remote --name <VM> exec /usr/bin/true
// (or any other vsock connection to port 9999)
//

import Foundation
import Darwin

// MARK: - vsock constants & structs

let AF_VSOCK: Int32 = 40
let VMADDR_CID_ANY: UInt32 = 0xFFFFFFFF

struct sockaddr_vm {
var svm_len: UInt8
var svm_family: UInt8
var svm_reserved1: UInt16
var svm_port: UInt32
var svm_cid: UInt32
var svm_zero: (UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0)

init(port: UInt32, cid: UInt32 = VMADDR_CID_ANY) {
self.svm_len = UInt8(MemoryLayout<sockaddr_vm>.size)
self.svm_family = UInt8(AF_VSOCK)
self.svm_reserved1 = 0
self.svm_port = port
self.svm_cid = cid
}
}

// MARK: - Create & bind server socket

let testPort: UInt32 = 9999

let serverFD = socket(AF_VSOCK, SOCK_STREAM, 0)
guard serverFD >= 0 else {
print("FAIL: socket() failed, errno=\(errno) (\(String(cString: strerror(errno))))")
print(" Are you running inside a macOS VM?")
exit(1)
}

var optval: Int32 = 1
setsockopt(serverFD, SOL_SOCKET, SO_REUSEADDR, &optval, socklen_t(MemoryLayout<Int32>.size))

var addr = sockaddr_vm(port: testPort)
let bindResult = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
Darwin.bind(serverFD, sockPtr, socklen_t(MemoryLayout<sockaddr_vm>.size))
}
}
guard bindResult == 0 else {
print("FAIL: bind() failed, errno=\(errno) (\(String(cString: strerror(errno))))")
close(serverFD)
exit(1)
}

guard listen(serverFD, 1) == 0 else {
print("FAIL: listen() failed, errno=\(errno)")
close(serverFD)
exit(1)
}

print("Listening on vsock port \(testPort)...")
print("Now connect from the host to trigger the test.")
print("")

// MARK: - Test 1: kqueue on the LISTEN socket (accept readiness)

print("=== Test 1: kqueue on listen socket (waiting for connection) ===")

let kq = kqueue()
guard kq >= 0 else {
print("FAIL: kqueue() failed, errno=\(errno)")
close(serverFD)
exit(1)
}

// Register EVFILT_READ on the server socket
var kev = kevent(
ident: UInt(serverFD),
filter: Int16(EVFILT_READ),
flags: UInt16(EV_ADD | EV_ENABLE),
fflags: 0,
data: 0,
udata: nil
)

let registerResult = kevent(kq, &kev, 1, nil, 0, nil)
if registerResult < 0 {
print("FAIL: kevent register failed, errno=\(errno) (\(String(cString: strerror(errno))))")
print(" kqueue does NOT support AF_VSOCK on this macOS version.")
close(kq)
close(serverFD)
exit(1)
}
print(" kevent register: OK (no error)")

// Wait for readability with a 30-second timeout
print(" Waiting up to 30s for kqueue to fire on listen socket...")
var timeout = timespec(tv_sec: 30, tv_nsec: 0)
var outEvent = kevent()
let nEvents = kevent(kq, nil, 0, &outEvent, 1, &timeout)

if nEvents < 0 {
print(" FAIL: kevent wait failed, errno=\(errno) (\(String(cString: strerror(errno))))")
close(kq)
close(serverFD)
exit(1)
} else if nEvents == 0 {
print(" TIMEOUT: kqueue did NOT fire within 30s.")
print(" Trying blocking accept() to see if a connection is actually pending...")

// Set non-blocking to test
let flags = fcntl(serverFD, F_GETFL, 0)
_ = fcntl(serverFD, F_SETFL, flags | O_NONBLOCK)
var clientAddr = sockaddr_vm(port: 0)
var addrLen = socklen_t(MemoryLayout<sockaddr_vm>.size)
let clientFD = withUnsafeMutablePointer(to: &clientAddr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
Darwin.accept(serverFD, sockPtr, &addrLen)
}
}
if clientFD >= 0 {
print(" RESULT: Connection WAS pending but kqueue didn't fire! kqueue BROKEN for vsock.")
close(clientFD)
} else {
print(" RESULT: No connection pending. Timed out waiting for a connection.")
print(" Connect from host and re-run to test.")
}
close(kq)
close(serverFD)
exit(1)
} else {
print(" kqueue FIRED! nEvents=\(nEvents)")
print(" filter=\(outEvent.filter) flags=\(outEvent.flags) data=\(outEvent.data)")
print(" RESULT: kqueue WORKS for AF_VSOCK listen sockets!")
}

close(kq)

// MARK: - Accept the connection

var clientAddr = sockaddr_vm(port: 0)
var addrLen = socklen_t(MemoryLayout<sockaddr_vm>.size)
let clientFD = withUnsafeMutablePointer(to: &clientAddr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
Darwin.accept(serverFD, sockPtr, &addrLen)
}
}
guard clientFD >= 0 else {
print("FAIL: accept() failed, errno=\(errno)")
close(serverFD)
exit(1)
}
print("\nAccepted connection (fd=\(clientFD))")

// MARK: - Test 2: kqueue on connected socket (data readiness)

print("\n=== Test 2: kqueue on connected socket (waiting for data) ===")

let kq2 = kqueue()
guard kq2 >= 0 else {
print("FAIL: kqueue() failed")
close(clientFD)
close(serverFD)
exit(1)
}

var kev2 = kevent(
ident: UInt(clientFD),
filter: Int16(EVFILT_READ),
flags: UInt16(EV_ADD | EV_ENABLE),
fflags: 0,
data: 0,
udata: nil
)

let reg2 = kevent(kq2, &kev2, 1, nil, 0, nil)
if reg2 < 0 {
print(" FAIL: kevent register on connected socket, errno=\(errno) (\(String(cString: strerror(errno))))")
} else {
print(" kevent register: OK")
}

// The host should be sending HTTP data, so wait for it
var timeout2 = timespec(tv_sec: 10, tv_nsec: 0)
var outEvent2 = kevent()
let nEvents2 = kevent(kq2, nil, 0, &outEvent2, 1, &timeout2)

if nEvents2 > 0 {
print(" kqueue FIRED on connected socket! data=\(outEvent2.data)")

// Try reading
var buf = [UInt8](repeating: 0, count: 4096)
let n = read(clientFD, &buf, buf.count)
if n > 0 {
let str = String(bytes: buf[0..<n], encoding: .utf8) ?? "<binary \(n) bytes>"
print(" Read \(n) bytes: \(str.prefix(200))")
}
print(" RESULT: kqueue WORKS for AF_VSOCK connected sockets!")
} else if nEvents2 == 0 {
print(" TIMEOUT: kqueue did NOT fire on connected socket within 10s")

// Check if data is actually available via blocking read
let flags = fcntl(clientFD, F_GETFL, 0)
_ = fcntl(clientFD, F_SETFL, flags | O_NONBLOCK)
var buf = [UInt8](repeating: 0, count: 4096)
let n = read(clientFD, &buf, buf.count)
if n > 0 {
print(" Data WAS available (\(n) bytes) but kqueue didn't fire! BROKEN.")
} else if n == 0 {
print(" EOF — connection closed by host before sending data")
} else {
print(" EAGAIN — no data pending. Host may not have sent anything yet.")
}
print(" RESULT: kqueue does NOT work for AF_VSOCK connected sockets.")
} else {
print(" FAIL: kevent wait error, errno=\(errno)")
}

// MARK: - Test 3: poll() on connected socket

print("\n=== Test 3: poll() on connected socket ===")

var pollFD = pollfd(fd: clientFD, events: Int16(POLLIN), revents: 0)
let pollResult = poll(&pollFD, 1, 5000) // 5s timeout

if pollResult > 0 {
print(" poll() returned \(pollResult), revents=\(pollFD.revents)")
print(" RESULT: poll() WORKS for AF_VSOCK!")
} else if pollResult == 0 {
print(" poll() timed out")
print(" RESULT: poll() does NOT work for AF_VSOCK.")
} else {
print(" poll() error, errno=\(errno)")
}

// MARK: - Test 4: DispatchSource read source

print("\n=== Test 4: DispatchSource.makeReadSource on connected socket ===")

// Write some data back to the client so the host side gets a response,
// then the host might close — we just want to see if DispatchSource fires

let semaphore = DispatchSemaphore(value: 0)
var dispatchSourceFired = false

let readSource = DispatchSource.makeReadSource(fileDescriptor: clientFD, queue: .global())
readSource.setEventHandler {
dispatchSourceFired = true
print(" DispatchSource FIRED! estimatedBytes=\(readSource.data)")
semaphore.signal()
}
readSource.setCancelHandler {
if !dispatchSourceFired {
print(" DispatchSource was cancelled without firing")
}
}
readSource.resume()

let waitResult = semaphore.wait(timeout: .now() + 5)
readSource.cancel()

if waitResult == .timedOut && !dispatchSourceFired {
print(" DispatchSource did NOT fire within 5s")
print(" RESULT: DispatchSource does NOT work for AF_VSOCK.")
} else if dispatchSourceFired {
print(" RESULT: DispatchSource WORKS for AF_VSOCK!")
}

// MARK: - Summary

print("\n=== Summary ===")
print("macOS version: \(ProcessInfo.processInfo.operatingSystemVersionString)")
print("Tests complete. See results above.")

close(kq2)
close(clientFD)
close(serverFD)
23 changes: 19 additions & 4 deletions macOS/GhostTools/Package.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
// swift-tools-version:5.9
// swift-tools-version:6.2
import PackageDescription

let package = Package(
name: "GhostTools",
platforms: [
.macOS(.v14)
.macOS("26.0")
],
products: [
.executable(name: "GhostTools", targets: ["GhostTools"])
],
dependencies: [],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.34.0"),
],
targets: [
.target(
name: "CPty",
path: "Sources/CPty",
publicHeadersPath: "include"
),
.executableTarget(
name: "GhostTools",
dependencies: [],
dependencies: [
"CPty",
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOWebSocket", package: "swift-nio"),
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
],
exclude: ["Resources/Info.plist", "Resources/Info.template.plist", "Resources/entitlements.plist"],
linkerSettings: [
.unsafeFlags(["-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", "../../build/generated-plists/GhostTools-Info.plist"], .when(configuration: .release))
Expand Down
1 change: 1 addition & 0 deletions macOS/GhostTools/Sources/CPty/cpty.c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Empty — we only need the header to expose forkpty/openpty to Swift
8 changes: 8 additions & 0 deletions macOS/GhostTools/Sources/CPty/include/cpty.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef CPTY_H
#define CPTY_H

#include <sys/ioctl.h>
#include <termios.h>
#include <util.h>

#endif
13 changes: 6 additions & 7 deletions macOS/GhostTools/Sources/GhostTools/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ struct GhostToolsApp: App {
final class AppDelegate: NSObject, NSApplicationDelegate {
private static let logger = Logger(subsystem: "org.ghostvm.ghosttools", category: "App")
private var statusItem: NSStatusItem?
private var server: VsockServer?
private var server: NIOVsockServer?
private var tunnelServer: TunnelServer?
private var healthServer: HealthServer?
private var isServerRunning = false
Expand Down Expand Up @@ -909,22 +909,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
print("[GhostTools] startServer() called")
Task {
do {
print("[GhostTools] Creating router...")
let router = Router()
print("[GhostTools] Creating VsockServer on port 5000...")
server = VsockServer(port: 5000, router: router)

server?.onStatusChange = { [weak self] running in
print("[GhostTools] Creating NIOVsockServer on port 5000...")
let srv = NIOVsockServer(port: 5000, router: router)
srv.onStatusChange = { [weak self] running in
Task { @MainActor in
print("[GhostTools] Server status changed: \(running)")
self?.isServerRunning = running
self?.updateStatusIcon(connected: running)
self?.updateMenu()
}
}
server = srv
try await srv.start()

print("[GhostTools] Starting vsock server...")
try await server?.start()
print("[GhostTools] Server started successfully")
} catch {
print("[GhostTools] Failed to start server: \(error)")
Expand Down
Loading