From a1bc3515db6212931237cae126200529011175be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 8 Jul 2026 23:26:53 +0800 Subject: [PATCH] Fix macOS bridge kernel panic --- protocol/bridge/backend_darwin.go | 32 ++++++++++++++----------------- protocol/bridge/backend_linux.go | 2 -- protocol/bridge/packet.go | 1 + protocol/bridge/rules_darwin.go | 13 +++++++++++-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/protocol/bridge/backend_darwin.go b/protocol/bridge/backend_darwin.go index e4ad445b7..2dd8d03e8 100644 --- a/protocol/bridge/backend_darwin.go +++ b/protocol/bridge/backend_darwin.go @@ -11,7 +11,6 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun/gtcpip/header" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -23,6 +22,16 @@ var ( bridgeInet6LocalBase = netip.MustParseAddr("2001:db8:1::1") ) +// macOS 26.5 (xnu-12377) kernel-panics ("Bounds safety trap") when pf +// route-to hands an unfragmented packet larger than one skywalk buflet to a +// skywalk-native interface: nx_netif_mbuf_to_kpkt() sizes the allocation +// against the TX pool, but nx_netif.c selects the copy routine from the RX +// pool's pp_max_frags, so pkt_copy_from_mbuf() writes past the 2048-byte +// buflet. utun_ctl_send() accepts writes of any size regardless of the +// interface MTU, so the limit must hold before packets are written; utun +// reserves UTUN_IF_HEADROOM_SIZE (32) bytes of the buflet, hence 2048-32. +const bridgeTunMTUDarwin = 2048 - 32 + type backendDarwin struct { backendBase @@ -83,13 +92,12 @@ func (b *backendDarwin) start() error { b.anchorName = "com.apple/sing-box-" + b.tunName tunInterface, err := tun.New(tun.Options{ Name: b.tunName, - MTU: bridgeTunMTU, + MTU: bridgeTunMTUDarwin, AutoRoute: false, InterfaceMonitor: b.networkManager.InterfaceMonitor(), Logger: b.logger, EXP_ExternalConfiguration: true, EXP_MultiPendingPackets: true, - EXP_SendMsgX: true, }) if err != nil { return E.Cause(err, "create bridge tun") @@ -126,7 +134,7 @@ func (b *backendDarwin) start() error { func (b *backendDarwin) startPlatform() error { session, err := b.platform.CreateBridge(adapter.BridgeOptions{ BridgeName: b.bridgeName, - MTU: bridgeTunMTU, + MTU: bridgeTunMTUDarwin, Inet4Port: b.inet4Port, Inet6Port: b.inet6Port, Interface: b.boundInterface, @@ -141,7 +149,7 @@ func (b *backendDarwin) startPlatform() error { } tunInterface, err := tun.New(tun.Options{ Name: b.tunName, - MTU: bridgeTunMTU, + MTU: bridgeTunMTUDarwin, FileDescriptor: session.FileDescriptor(), Logger: b.logger, EXP_ExternalConfiguration: true, @@ -207,10 +215,8 @@ func (b *backendDarwin) Close() error { return nil } -// Zero tells the dispatcher not to clamp the TCP MSS or fragment; pf and the -// host kernel do both on the forwarding path instead (see buildBridgeAnchorRules). func (b *backendDarwin) PortMTU() uint32 { - return 0 + return bridgeTunMTUDarwin } func (b *backendDarwin) WritePackets(packets [][]byte) error { @@ -224,18 +230,8 @@ func (b *backendDarwin) WritePackets(packets [][]byte) error { packets = packets[len(chunk):] batch := b.writeBatch[:0] for _, packet := range chunk { - if len(packet) == 0 || len(packet) > maxPacketLength { - continue - } - ipVersion := header.IPVersion(packet) - if ipVersion != header.IPv4Version && ipVersion != header.IPv6Version { - continue - } batch = append(batch, buf.As(packet)) } - if len(batch) == 0 { - continue - } err := b.batchTUN.BatchWrite(batch) if err != nil { return err diff --git a/protocol/bridge/backend_linux.go b/protocol/bridge/backend_linux.go index 734c6a5df..c617c0bdd 100644 --- a/protocol/bridge/backend_linux.go +++ b/protocol/bridge/backend_linux.go @@ -260,8 +260,6 @@ func (b *backendLinux) Close() error { return nil } -// Zero tells the dispatcher not to clamp the TCP MSS or fragment; the host kernel -// does both on the forwarding path instead (see setupBridgeClampRules). func (b *backendLinux) PortMTU() uint32 { return 0 } diff --git a/protocol/bridge/packet.go b/protocol/bridge/packet.go index f9e2f4159..47f089e4b 100644 --- a/protocol/bridge/packet.go +++ b/protocol/bridge/packet.go @@ -1,5 +1,6 @@ //go:build linux || darwin || (windows && (amd64 || 386)) +//nolint:unused package bridge import ( diff --git a/protocol/bridge/rules_darwin.go b/protocol/bridge/rules_darwin.go index fca20e307..17fcd56e7 100644 --- a/protocol/bridge/rules_darwin.go +++ b/protocol/bridge/rules_darwin.go @@ -17,9 +17,18 @@ func buildBridgeAnchorRules(ruleLogger logger.ContextLogger, tunName string, egr if err != nil { return nil } + // The flowswitch aggregates forwarded TCP into packets larger than the tun + // MTU, and pf_route() only fragments when they exceed the egress MTU: a + // large-MTU utun target feeds them whole into the overflow described at + // bridgeTunMTUDarwin. + if egressInterface.Flags&net.FlagBroadcast == 0 || egressInterface.Flags&net.FlagLoopback != 0 || + egressInterface.Flags&net.FlagPointToPoint != 0 { + ruleLogger.Error("bridge egress ", egress, " is not a physical interface, dropping forwarded traffic") + return nil + } mtu := egressInterface.MTU - if mtu < 576 || mtu > bridgeTunMTU { - mtu = bridgeTunMTU + if mtu < 576 || mtu > bridgeTunMTUDarwin { + mtu = bridgeTunMTUDarwin } localPrefixes, inet4Interfaces, inet6Interfaces := collectLocalSegments(egress, boundInterface, inet4Port.IsValid(), inet6Port.IsValid()) var rules []pfAnchorRule