Normale weergave
Amazon Launches Early Prime Day Discounts on AirPods, iPad, MacBook Air, and More
As is typical for Prime Day deals, these markdowns are very time sensitive, so sales listed below may disappear fast, and new ones may appear even faster. With this in mind, we'll keep this article updated throughout the event, and keep an eye on the MacRumors front page as we'll be posting particularly great deals in separate articles this week.
Lastly, and perhaps most importantly, Amazon Prime Day requires you to have an Amazon Prime membership to take advantage of the discounts. Amazon Prime costs $14.99 per month or $139.00 per year, and it comes with a 30-day free trial for new subscribers.
AirPods
The best AirPods discount so far this Prime Day is on the AirPods Pro 3, available for the record low price of $179.00, down from $249.00. We haven't seen a deal this good since over the summer, during the first Prime Day event in June.
MacBook Air
Amazon is taking $200 off nearly every M5 MacBook Air model for Prime Big Deal Days, including both 13-inch and 15-inch models. These are the best prices we've seen on the M5 MacBook Air in the wake of Apple's summertime price hikes.
In terms of 13-inch models, Amazon has the 16GB/512GB 13-inch MacBook Air for $1,099.00, down from $1,299.00. This one is available in three colors on Amazon, and it's accompanied by the 16GB/1TB and 24GB/1TB model for $200 off as well.
For the 15-inch M5 MacBook Air models, prices start with the 16GB/512GB model for $1,299.00, down from $1,499.00. You can also get the 16GB/1TB model for $1,599.00, down from $1,799.00, all $200 off and the best prices we've seen in months.
Apple Watch
Rare deals have hit the Apple Watch SE 3 for Prime Big Deal Days, with prices now starting at $199.00 for the 40mm GPS model and increasing to $229.00 for the 44mm GPS model. Both of these are $50 discounts and matches for the record low prices on each device.
For the Apple Watch Series 12, you can get the 42mm GPS model for $349.99 and the 46mm GPS model for $399.99, both of which are $49 discounts and new all-time low prices. Both of these models have two colors available at these all-time low prices, and you'll find a few cellular options on sale as well.
For the Apple Watch Ultra 4, you can get the Natural model with the Translucent Gray Ocean Band for $699.99, down from $799.00. This is another new all-time low price for Prime Day, and as of writing it's the only color we're tracking at this price. Additionally, there are a few Milanese Loop models on sale for $99 off ahead of Prime Day.
M4 iPad Air
Amazon this week has multiple discounts on the M4 iPad Air, providing up to $149 off these 2026 models. These remain the best prices we've seen in the wake of Apple's summer price hikes, and many deals are being matched at Best Buy.
The new iPad Air features the M4 chip, C1X modem, and N1 networking chip, which brings support for Wi-Fi 7 and Bluetooth 6. In terms of design, the 2026 models are identical to the 2025 iPad Air tablets, with an edge-to-edge display, slim bezels, and aluminum chassis.
11-inch M4 iPad Air
- 128GB Wi-Fi - $649.00 ($100 off)
- 256GB Wi-Fi - $749.00 ($100 off)
- 512GB Wi-Fi - $949.00 ($100 off)
- 256GB Cellular - $899.00 ($100 off)
- 1TB Cellular - $1,399.00 ($100 off)
13-inch M4 iPad Air
- 128GB Wi-Fi - $849.00 ($100 off)
- 256GB Wi-Fi - $949.00 ($100 off)
- 1TB Wi-Fi - $1,449.00 ($100 off)
iPad Mini 7
Amazon has low prices on every Wi-Fi model of the iPad mini 7, starting at $499.00 for the 128GB Wi-Fi tablet, down from $599.00. You'll also find a few deals on cellular models during this early Prime Day sale.
If you're on the hunt for more discounts, be sure to visit our Apple Deals roundup where we recap the best Apple-related bargains of the past week.
Deals Newsletter
Interested in hearing more about the best deals you can find in 2026? Sign up for our Deals Newsletter and we'll keep you updated so you don't miss the biggest deals of the season!
This article, "Amazon Launches Early Prime Day Discounts on AirPods, iPad, MacBook Air, and More" first appeared on MacRumors.com
Discuss this article in our forums
AirPods Pro 3 Hit Record Low $179 Price During Early Prime Day Sale
This model of the AirPods Pro launched in September 2025 and has 2x better Active Noise Cancellation than the previous generation, better audio quality, a revised fit that's meant to improve comfort and stability, Live Translation for in-person conversations, and heart rate sensing for workouts.
Head to our full Deals Roundup to get caught up with all of the latest deals and discounts that we've been tracking over the past week.
Deals Newsletter
Interested in hearing more about the best deals you can find in 2026? Sign up for our Deals Newsletter and we'll keep you updated so you don't miss the biggest deals of the season!
This article, "AirPods Pro 3 Hit Record Low $179 Price During Early Prime Day Sale" first appeared on MacRumors.com
Discuss this article in our forums
Vincent Bernat: Hacking the Go compiler to efficiently map IPv4 to IPv6
netip.Addr features an Unmap()
method returning the unwrapped IPv4 contained in an IPv4-mapped IPv6
address: from ::ffff:203.0.113.10 or ::ffff:cb00:710a, it returns
203.0.113.10.1 There is no Map() or To6() method for the reverse
direction. Such a method is trivial to implement, but Go maintainers have
rejected it on the grounds that users should write
netip.AddrFrom16(ip.As16()) and let the compiler optimize it.2 Today,
this pattern is eight times slower than a native method. How can we teach the
compiler to optimize this sequence?
The alternatives
Letβs explore three ways to implement the map semantics for netip.Addr. My
favorite is to add it to the Go standard library. Go maintainers prefer a small
external helper chaining netip.AddrFrom16() and netip.Addr.As16(), hoping
the compiler eventually optimizes it. The unsafe package
opens a third path, with the same performance as the first solution.
Modifying the Go standard library
Internally, netip.Addr stores any IP address as a
128-bit value with an extra field z to encode the family and the zone:
type Addr struct {
addr uint128
z unique.Handle[addrDetail]
}
type addrDetail struct {
isV6 bool // IPv4 is false, IPv6 is true.
zoneV6 string // != "" only if IsV6 is true.
}
var (
z0 unique.Handle[addrDetail]
z4 = unique.Make(addrDetail{})
z6noz = unique.Make(addrDetail{isV6: true})
)
AddrFrom4() encodes an IPv4 address as an
IPv4-mapped IPv6 address and sets z to the unique value z4:
// AddrFrom4 returns the address of the IPv4 address given by the bytes in addr.
func AddrFrom4(addr [4]byte) Addr {
return Addr{
addr: uint128{
0,
0xffff00000000 |
uint64(addr[0])<<24 | uint64(addr[1])<<16 |
uint64(addr[2])<<8 | uint64(addr[3])},
z: z4,
}
}
Unmap() turns an IPv4-mapped IPv6 address into an
IPv4 address by setting the z field to z4:
func (ip Addr) Unmap() Addr {
if ip.Is4In6() {
ip.z = z4
}
return ip
}
Implementing the reverse direction inside the Go standard library is trivial: we
set the z field to z6noz if the address is IPv4.
// To6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func (ip Addr) To6() Addr {
if ip.Is4() {
ip.z = z6noz
}
return ip
}
Update (2026-10)
Instead of patching Go, we can use the -overlay
flag of go build to hijack any package, including the standard
library. In our case, it can add To6() to net/netip. Yet
it is cumbersome:3 the flag expects a JSON file mapping absolute
paths in GOROOT to replacement files, and you need to add it to every go
command.
As a helper
We canβt access the z field from outside the net/netip package. Instead, we
build a small helper around the netip.AddrFrom16(ip.As16()) pattern:
// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func AddrTo6(ip netip.Addr) netip.Addr {
if ip.Is4() {
ip = netip.AddrFrom16(ip.As16())
}
return ip
}
As an unsafe function
Another solution uses the unsafe package to alter the Addr struct through
a proxy with the same memory layout:4
// addrProxy has the same memory layout as netip.Addr.
type addrProxy struct {
addr [2]uint64 // netip.uint128
z unsafe.Pointer // unique.Handle[netip.addrDetail]
}
var (
anyIPv6 = netip.IPv6Unspecified()
netipZ6noz = (*addrProxy)(unsafe.Pointer(&anyIPv6)).z
)
// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func AddrTo6(ip netip.Addr) netip.Addr {
if !ip.Is4() {
return ip
}
(*addrProxy)(unsafe.Pointer(&ip)).z = netipZ6noz
return ip
}
Benchmarks
On my computer, with Goβ―1.27.1, the standard library solution costs 0.88 ns per operation, while the solution favored by Go maintainers costs 7.14 ns. The unsafe solution matches the performance of the first one.
goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
β sec/op β
AddrTo6/safe 7.137n Β± 0%
AddrTo6/unsafe 0.8682n Β± 2%
AddrTo6/builtin 0.8775n Β± 2%
Assembly code
Letβs check the assembly code the compiler generates for each solution.5 The one built into the standard library looks like this:6
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
Goβs assembly language is not a direct representation of the underlying
machine language: it operates on a semi-abstract instruction set derived from
Planβ―9βs assembler. It has four pseudo-registers: FP (frame pointer
for function arguments), PC (program counter), SB (static base pointer for
global symbols), and SP (stack pointer). It also has architecture-specific
registers like AX, CX, DX, BX, SI, DI, and R8 to R15.
Instructions storing data use their last argument as the destination.
Instructions can carry an explicit size suffix: MOVB moves a byte, MOVWβ―16
bits, MOVLβ―32 bits, and MOVQβ―64 bits. In the example above, the first
instruction compares the 64-bit value z4 with the CX register.
The unsafe solution looks almost the same:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ netipZ6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The helper solution has far more instructions. To understand why, letβs look at
the code for As16() and
AddrFrom16(). They are short enough for the compiler
to inline them.
func (ip Addr) As16() (a16 [16]byte) {
byteorder.BEPutUint64(a16[:8], ip.addr.hi)
byteorder.BEPutUint64(a16[8:], ip.addr.lo)
return a16
}
func AddrFrom16(addr [16]byte) Addr {
return Addr{
addr: uint128{
byteorder.BEUint64(addr[:8]),
byteorder.BEUint64(addr[8:]),
},
z: z6noz,
}
}
We can already guess the pattern to optimize: the code packs the IP address into an array, copies it, then unpacks it. If we inline the Go code by hand, we get:
func AddrTo6(input netip.Addr) netip.Addr {
if !input.Is4() {
return input
}
var a16 [16]byte
byteorder.BEPutUint64(a16[:8], input.addr.hi)
byteorder.BEPutUint64(a16[8:], input.addr.lo)
addr := a16
var output netip.Addr
output.addr.hi = byteorder.BEUint64(addr[:8])
output.addr.lo = byteorder.BEUint64(addr[8:])
output.z = netip.z6noz
return output
}
As humans, we can mentally derive the optimized form:
func AddrTo6(input netip.Addr) netip.Addr {
if !input.Is4() {
return input
}
var output netip.Addr
output.addr.hi = input.addr.hi
output.addr.lo = input.addr.lo
output.z = netip.z6noz
return output
}
Unfortunately, as of Goβ―1.26.8, the compiler is not smart enough to do the same:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
; Push the stack (32 bytes):
; 0(SP) addr netip.uint128
; 16(SP) a16 [16]byte
PUSHQ BP
MOVQ SP, BP
SUBQ $32, SP
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)
; byteorder.BEPutUint64(a16[8:], input.addr.lo)
MOVBEQ AX, net/netipΒ·a16+16(SP)
MOVBEQ BX, net/netipΒ·a16+24(SP)
; addr = a16, 16 bytes at once through the vector register X0
MOVUPS net/netipΒ·a16+16(SP), X0
MOVUPS X0, net/netipΒ·addr(SP)
; CX = netip.z6noz
MOVQ net/netipΒ·z6noz(SB), CX
; Unpack: output.addr.hi = byteorder.BEUint64(addr[:8])
; output.addr.lo = byteorder.BEUint64(addr[8:])
MOVBEQ net/netipΒ·addr(SP), AX
MOVBEQ net/netipΒ·addr+8(SP), BX
end:
ADDQ $32, SP
POPQ BP
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The compiler does a decent job on the byte shuffling: the eight byte stores of
BEPutUint64() become a single MOVBEQ, which stores a register byte-swapped.
The eight byte loads of BEUint64() become a single MOVBEQ the other way
round.7 Three groups of instructions remain: a pack, a copy, and an
unpack.
Hacking the Go compiler
The Go compiler has several phases:
- Parsing
- The compiler tokenizes and parses the source code. It builds a syntax tree for each source file.
- Type checking
- The compiler maps each identifier to the object it denotes, folds constants, and infers the type of every expression.
- IR construction
- The compiler converts the syntax tree and its types into its own intermediate representation (IR). This process, called βnoding,β goes through a serialization format named unified IR.
- Middle end
- The compiler performs several optimization passes on the IR, such as devirtualization, function call inlining, and escape analysis.
- Walk
- This phase runs two steps: order of evaluation decomposes complex
statements into simpler ones, and desugaring transforms higher-level Go
constructs, like
switchor channels, into more primitive instructions or calls to the runtime. - Generic SSA
- The compiler converts the IR into Static Single Assignment (SSA) form, a lower-level intermediate representation suited for machine-independent optimizations and rewrite rules.
- Machine code generation
- The compiler rewrites the SSA form into machine-specific variants, allocates registers, and applies more optimization passes. At the end, the assembler turns the generated instructions into machine code.
The hammer
My first idea is to replace occurrences of netip.AddrFrom16(ip.As16()) with
netip.Addr{addr: ip.addr, z: netip.z6noz} as early as possible, during the
βnodingβ process. Before that, the type checking phase prevents
us from accessing unexported struct fields.
Goβ―1.27 introduced a convenient debug option to dump the IR of a function at interesting points during compilation:
$ GOTOOLCHAIN=go1.27.1 GOAMD64=v3 go build -a -gcflags="-d=astdump=AddrTo6Safe" .
Writing text ast output for AddrTo6Safe to AddrTo6Safe.ast
Writing html ast output for AddrTo6Safe to AddrTo6Safe.html
Writing html syntax output for AddrTo6Safe to AddrTo6Safe.syntax.html
In the HTML file, the first column shows the IR as it comes out of noding:
DCLFUNC addrto6.AddrTo6Safe ABI:ABIInternal FUNC-func(netip.Addr) netip.Addr
DCLFUNC-Dcl
. NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. NAME-addrto6.~r0 Class:PPARAMOUT Offset:0 OnStack netip.Addr
DCLFUNC-body
. IF # ipv6_safe.go:11:2
. IF-Cond
. . CALLFUNC bool
. . CALLFUNC-Fun
. . . METHEXPR addrto6.Is4 FUNC-func(netip.Addr) bool
. . . . TYPE netip.Addr Class:PEXTERN Offset:0 type netip.Addr
. . CALLFUNC-Args
. . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. IF-Body
. . AS # ipv6_safe.go:12:6
. . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . . CALLFUNC netip.Addr
. . . CALLFUNC-Fun
. . . . NAME-netip.AddrFrom16 Class:PFUNC Offset:0 Used FUNC-func([16]byte) netip.Addr
. . . CALLFUNC-Args
. . . . CALLFUNC ARRAY-[16]byte
. . . . CALLFUNC-Fun
. . . . . METHEXPR addrto6.As16 FUNC-func(netip.Addr) [16]byte
. . . . . . TYPE netip.Addr Class:PEXTERN Offset:0 type netip.Addr
. . . . CALLFUNC-Args
. . . . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. RETURN # ipv6_safe.go:14:2
. RETURN-Results
. . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
In the body of the if statement, we spot the calls to the method
netip.Addr.As16() and to the function netip.AddrFrom16(). Our goal is to
patch them with a struct literal:
IF-Body
. AS # ipv6_safe.go:12:6
. . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . STRUCTLIT netip.Addr
. . STRUCTLIT-List
. . . STRUCTKEY netip.addr
. . . . DOT netip.addr netip.uint128
. . . . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . . STRUCTKEY netip.z
. . . . NAME-netip.z6noz Class:PEXTERN Offset:0 unique.Handle[net/netip.addrDetail]
In noderβs reader.go, the expr() method builds the IR tree for
an expression. At the end of the exprCall case, we add a call to a
rewriteAddrFrom16As16() function. It takes the current node and returns the
struct literal on success, or nil if the rewrite is not possible. First, we
check that we have the expected pattern: a call to the netip.AddrFrom16()
function with a call to the netip.Addr.As16() method as its only argument:
func rewriteAddrFrom16As16(n ir.Node) ir.Node {
call, ok := n.(*ir.CallExpr)
if !ok || call.Op() != ir.OCALLFUNC ||
len(call.Args) != 1 || len(call.Init()) != 0 ||
!isNetipFunc(call.Fun, "AddrFrom16") {
return nil
}
inner, ok := call.Args[0].(*ir.CallExpr)
if !ok || inner.Op() != ir.OCALLFUNC ||
len(inner.Args) != 1 || len(inner.Init()) != 0 ||
!isNetipFunc(inner.Fun, "Addr.As16") {
return nil
}
x := inner.Args[0]
// [...]
}
Then, we fetch netip.z6noz:
z6noz, err := lookupVar(ir.StaticCalleeName(call.Fun).Sym().Pkg, "z6noz")
if err != nil {
return nil
}
And we build the struct literal:
typ := call.Type()
pos := call.Pos()
var list []ir.Node
for i, f := range typ.Fields() {
var value ir.Node
switch f.Sym.Name {
case "addr":
value = typecheck.DotField(pos, x, i)
case "z":
value = z6noz
default:
return nil
}
list = append(list, ir.NewStructKeyExpr(pos, f, value))
}
lit := ir.NewCompLitExpr(pos, ir.OSTRUCTLIT, typ, list)
lit.SetTypecheck(1)
return lit
Have a look at the complete patch.8 We can test it with the following commands:
$ cd src
$ ./make.bash
Building Go cmd/dist using /usr/lib/go-1.27. (go1.27.1 linux/amd64)
Building Go toolchain1 and bootstrap cmd/go (go_bootstrap) using /usr/lib/go-1.27.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 and commands using go_bootstrap and Go toolchain2.
Checking command staleness for linux/amd64.
---
Installed Go for linux/amd64 in /home/bernat/code/free/go
Installed commands in /home/bernat/code/free/go/bin
*** You need to add /home/bernat/code/free/go/bin to your PATH.
$ export PATH=$PWD/../bin:$PATH
$ go version
go version go1.28-devel_9834516e20 Sat Sep 12 08:23:11 2026 -0700 linux/amd64
$ go test net/netip/...
ok net/netip 0.224s
$ cd ../../go-netip-addrto6
$ go test .
ok github.com/vincentbernat/go-netip-addrto6 0.062s
The generated code for the helper is now the shortest possible version!
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
Go maintainers are unlikely to accept this change. It relies on the internal
structure of net/netip.Addr. Itβs an ugly hack in the noder, whose job is to
faithfully translate the type-checked AST into the IR. And itβs harder to
maintain than adding a To6() method.
The screwdriver
The right place for such an optimization is the generic SSA phase. One of
the last machine-independent passes is memcombine. With the appropriate debug
flag, the compiler dumps the SSA form after this pass:9
$ GOTOOLCHAIN=go1.26.8 GOAMD64=v3 \
> go build -a -gcflags='-d=ssa/memcombine/dump=AddrTo6Safe' .
$ head -5 AddrTo6Safe_01__memcombine.dump
AddrTo6Safe func(netip.Addr) netip.Addr
b2:
(?) v1 = InitMem <mem>
(?) v2 = SP <uintptr>
(?) v3 = SB <uintptr>
The result of the memcombine pass follows the same structure as the assembly
code for AddrTo6Safe() we looked at earlier: two stores,
one move, and two loads we would like to optimize away.
; [β¦]
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v466 = ArgIntReg <*netip.addrDetail> {ip+16} [2] ; input.z
; [β¦]
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo)
v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi)
v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v415 = OffPtr <*byte> [8] v285 ; &addr[8]
v416 = Load <uint64> v285 v286 ; addr[:8]
v299 = Bswap64 <uint64> v416 ; output.addr.hi
v174 = Load <uint64> v415 v286 ; addr[8:]
v39 = Bswap64 <uint64> v174 ; output.addr.lo
; [β¦]
Each line features a value identifier (v442), an operation with its type
(Bswap64 <uint64>), and its arguments (v490).10 Values are the basic
building blocks of SSA and are defined exactly once. Square brackets enclose
integer parameters ([8]) and curly braces contain auxiliary arguments
({netip.addr}). Operations writing to memory produce a new memory state. Every
memory operation takes the current state as its last argument, which keeps them
in order.
On paper
Letβs focus on output.addr.lo, aka v39:
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v415 = OffPtr <*byte> [8] v285 ; &addr[8]
v174 = Load <uint64> v415 v286 ; addr[8:]
v39 = Bswap64 <uint64> v174 ; output.addr.lo
To simplify this code, we could apply three rewriting rules:
-
The first one adds a shortcut when loading through a move:
(Load (OffPtr [o] p) (Move p src mem)) => (Load (OffPtr [o] src) mem). This matchesv174with its argumentsv415andv286and creates a new valuev600:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Load <uint64> v600 v282 ; a16[8:] v39 = Bswap64 <uint64> v174 ; output.addr.lo -
The second one simplifies a load following a store:
(Load p (Store p x _)) => x. The load is forwarded: the stored value replaces it and no memory access remains. It matchesv174. It notices thatv600andv173are the same address and replacesv174with a copy ofv442:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Copy <uint64> v442 ; bswap(input.addr.lo) v39 = Bswap64 <uint64> v174 ; output.addr.lo -
The last step cancels the two byte swaps:
(Bswap64 (Bswap64 x)) => x.v39becomes a copy ofv490:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Copy <uint64> v442 ; bswap(input.addr.lo) v39 = Copy <uint64> v490 ; output.addr.lo = input.addr.lo
If we ignore the values not needed to compute v39, only this SSA form remains:
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v39 = Copy <uint64> v490 ; output.addr.lo = input.addr.lo
Letβs switch to output.addr.hi, aka v299:
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi)
v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v416 = Load <uint64> v285 v286 ; addr[:8]
v299 = Bswap64 <uint64> v416 ; output.addr.hi
To optimize it away, we also apply three rewriting rules:
-
The first one also adds a shortcut when loading through a move, but without an offset:
(Load p (Move p src mem)) => (Load src mem). This rewritesv416to use arguments fromv286:v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Load <uint64> v22 v282 ; a16[:8] v299 = Bswap64 <uint64> v416 ; output.addr.hi -
The second rule forwards a value stored one step earlier, skipping over a store to another address:
(Load p (Store q _ (Store p x _))) => x. This matchesv416:xisv542,pisv22(&a16),qisv173(&a16[8]), andpandqdo not overlap foruint64.v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Copy <uint64> v542 ; bswap(input.addr.hi) v299 = Bswap64 <uint64> v416 ; output.addr.hi -
The third rule cancels two byte swaps:
(Bswap64 (Bswap64 x)) => x.v299becomes a copy ofv502:v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Copy <uint64> v542 ; bswap(input.addr.hi) v299 = Copy <uint64> v502 ; output.addr.hi = input.addr.hi
If we remove the values not used to compute v299, we get this SSA form:
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v299 = Copy <uint64> v502 ; output.addr.hi = input.addr.hi
In practice
Most of these rules already exist in generic.rules. They use
conditions to validate their context: ssa.IsSamePtr() for the same address,
ssa.Disjoint() for addresses that do not overlap. The rule forwarding a stored
value to a load already exists with three variants looking through several other
stores. Here are the two we need:
(Load <t1> p1 (Store {t2} p2 x _))
&& ssa.IsSamePtr(p1, p2)
&& copyCompatibleType(t1, x.Type)
&& t1.Size() == t2.Size()
=> x
(Load <t1> p1 (Store {t2} p2 _ (Store {t3} p3 x _)))
&& ssa.IsSamePtr(p1, p3)
&& copyCompatibleType(t1, x.Type)
&& t1.Size() == t3.Size()
&& ssa.Disjoint(p3, t3, p2, t2)
=> x
Goβ―1.27 added the rule loading through a move with CLβ―748200 to fix issueΒ #77720:
(Load <t1> op1:(OffPtr [o1] p1) move:(Move [n] p2 src mem))
&& o1 >= 0 && o1+t1.Size() <= n && ssa.IsSamePtr(p1, p2)
&& !ssa.IsVolatile(src)
=> @move.Block (Load <t1> (OffPtr <op1.Type> [o1] src) mem)
It lacks a variant without an offset:
(Load <t1> p1 move:(Move [n] p2 src mem))
&& p1.Op != ssaop.OpOffPtr
&& t1.Size() <= n && ssa.IsSamePtr(p1, p2)
&& !ssa.IsVolatile(src)
=> @move.Block (Load <t1> (OffPtr <p1.Type> [0] src) mem)
There is no generic rule to cancel two byte swaps, but the AMD64 lowering pass includes this rule:
(BSWAP(Q|L) (BSWAP(Q|L) p)) => p
After switching to Goβs development branch and adding the missing rule, the generated assembly code is worse than with Goβ―1.26.8, even though our additional rule slightly improves the situation at the end:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
; Push the stack (16 bytes):
; 0(SP) a16 [16]byte
PUSHQ BP
MOVQ SP, BP
SUBQ $16, SP
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
; The four forwarded bytes: the low half of input.addr.lo is taken apart and
; put back together in registers
MOVQ BX, DX ; DX = input.addr.lo
SHRQ $24, BX ; BX = input.addr.lo >> 24
MOVQ DX, SI ; SI = input.addr.lo
SHRQ $16, DX ; DX = input.addr.lo >> 16
MOVQ SI, DI ; DI = input.addr.lo, kept for the pack
SHRQ $8, SI ; SI = input.addr.lo >> 8
MOVBLZX DIB, R8 ; R8 = byte(input.addr.lo)
MOVBLZX SIB, SI ; SI = byte(input.addr.lo >> 8)
SHLQ $8, SI
ORQ R8, SI ; SI = two low bytes of input.addr.lo
MOVBLZX DL, DX ; DX = byte(input.addr.lo >> 16)
SHLQ $16, DX
ORQ SI, DX
MOVBLZX BL, BX ; BX = byte(input.addr.lo >> 24)
SHLQ $24, BX
ORQ DX, BX ; BX = input.addr.lo & 0xffffffff
; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)
; byteorder.BEPutUint64(a16[8:], input.addr.lo)
MOVBEQ AX, net/netipΒ·a16(SP)
MOVBEQ DI, net/netipΒ·a16+8(SP)
; The four other bytes of input.addr.lo, read one by one from a16
MOVBLZX net/netipΒ·a16+11(SP), DX ; a16[11]
SHLQ $32, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+10(SP), DX ; a16[10]
SHLQ $40, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+9(SP), DX ; a16[9]
SHLQ $48, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+8(SP), DX ; a16[8]
SHLQ $56, DX
; output.z = netip.z6noz
MOVQ net/netipΒ·z6noz(SB), CX
; output.addr.hi = byteorder.BEUint64(a16[:8])
MOVBEQ net/netipΒ·a16(SP), AX
; output.addr.lo assembled from the previous steps
ORQ DX, BX
end:
LEAVEQ
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The rule loading through a move, added in Goβ―1.27, introduced this regression.
Out of order
Letβs not give up now! In reality, the rewriting rules run before memcombine,
notably in the late opt pass. At this point, the inlined versions of
BEPutUint64() and BEUint64() still expand to sixteen byte stores and sixteen
byte loads, matching their source code:
func BEUint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}
Letβs follow two bytes of output.addr.lo: addr[15] and addr[11]. Here is
a simplified SSA form before late opt:
v273 = Trunc64to8 <byte> v490 ; byte(input.addr.lo)
v226 = Trunc64to8 <byte> v225 ; byte(input.addr.lo >> 32)
; [β¦]
v235 = Store <mem> {byte} v233 v226 v223 ; a16[11] = byte(lo >> 32)
v247 = Store <mem> {byte} v245 v238 v235 ; a16[12] = β¦
v259 = Store <mem> {byte} v257 v250 v247 ; a16[13] = β¦
v271 = Store <mem> {byte} v269 v262 v259 ; a16[14] = β¦
v282 = Store <mem> {byte} v280 v273 v271 ; a16[15] = byte(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
; [β¦]
v433 = OffPtr <*byte> [15] v285 ; &addr[15]
v435 = Load <byte> v433 v286 ; addr[15]
v479 = OffPtr <*byte> [11] v285 ; &addr[11]
v481 = Load <byte> v479 v286 ; addr[11]
The first rule loads through the move: (Load (OffPtr [o] p) (Move p src mem))
=> (Load (OffPtr [o] src) mem). It matches both loads, which now read a16
with the memory state before the copy:
v600 = OffPtr <*byte> [15] v22 ; &a16[15]
v435 = Load <byte> v600 v282 ; a16[15]
v601 = OffPtr <*byte> [11] v22 ; &a16[11]
v481 = Load <byte> v601 v282 ; a16[11]
The second rule shortcuts a load following a store: (Load p (Store p x _)) =>
x. It matches v435, as v282 stores a16[15]. It does not match v481:
v235 stores a16[11] four stores earlier in the chain, while the variants of
this rule look through three stores at most.
v435 = Copy <byte> v273 ; byte(input.addr.lo)
v601 = OffPtr <*byte> [11] v22 ; &a16[11]
v481 = Load <byte> v601 v282 ; a16[11]
The same happens to the other bytes: the rule forwards the four bytes stored
last, a16[12] to a16[15]. The twelve other loads now read a16 instead of
addr.
BEUint64() becomes a chain of Or64, each one adding a byte shifted into
place. memcombine is a pass written in Go, not a set of rewrite
rules. It starts from the last Or64 of the chain and collects up to eight
terms. If each term is a byte load, extended to 64 bits and shifted, and if the
eight loads read consecutive addresses from the same pointer with the same
memory state, it replaces the whole chain with a single 64-bit load and a byte
swap. Otherwise, it tries again with four, then two terms, and from each
intermediate Or64. Here is the loop checking each term in a simplified version
of combineLoads():
for i := int64(0); i < n; i++ {
v := a[i]
shift := int64(0)
if v.Op == shiftOp {
v, shift = peelShift(v)
}
if v.Op != extOp {
return false
}
load := v.Args[0]
if load.Op != ssaop.OpLoad {
return false
}
if load.Args[1] != mem {
return false
}
p, off := splitPtr(load.Args[0])
if p != base {
return false
}
r[i] = LoadRecord{load: load, offset: off, shift: shift}
}
For output.addr.hi, the eight loads read a16 with the same memory state
v282:
v13 = Load <byte> v22 v282 ; a16[0]
v530 = Load <byte> v14 v282 ; a16[1]
v488 = Load <byte> v504 v282 ; a16[2]
v405 = Load <byte> v537 v282 ; a16[3]
v385 = Load <byte> v397 v282 ; a16[4]
v361 = Load <byte> v373 v282 ; a16[5]
v196 = Load <byte> v63 v282 ; a16[6]
v432 = Load <byte> v315 v282 ; a16[7]
v319 = ZeroExt8to64 <uint64> v432 ; uint64(a16[7])
v329 = ZeroExt8to64 <uint64> v196 ; uint64(a16[6])
v330 = Lsh64x64 <uint64> [true] v329 v138 ; uint64(a16[6]) << 8
v331 = Or64 <uint64> v319 v330 ; a16[7] | a16[6] << 8
; [β¦] same for a16[5] to a16[1]
v401 = ZeroExt8to64 <uint64> v13 ; uint64(a16[0])
v402 = Lsh64x64 <uint64> [true] v401 v55 ; uint64(a16[0]) << 56
v403 = Or64 <uint64> v402 v391 ; | a16[0] << 56 = output.addr.hi
memcombine merges them into one load and a swap:
v286 = Load <uint64> v22 v282 ; a16[:8]
v285 = Bswap64 <uint64> v286 ; output.addr.hi
For output.addr.lo, here is the chain memcombine sees after late opt:
v436 = ZeroExt8to64 <uint64> v273 ; addr[15], forwarded
v448 = Or64 <uint64> v436 v447 ; | addr[14] << 8, forwarded
v460 = Or64 <uint64> v459 v448 ; | addr[13] << 16, forwarded
v472 = Or64 <uint64> v471 v460 ; | addr[12] << 24, forwarded
v484 = Or64 <uint64> v483 v472 ; | a16[11] << 32, loaded
v496 = Or64 <uint64> v495 v484 ; | a16[10] << 40, loaded
v508 = Or64 <uint64> v507 v496 ; | a16[9] << 48, loaded
v520 = Or64 <uint64> v519 v508 ; | a16[8] << 56, loaded
From v520, four of the eight terms are forwarded bytes, not loads from memory,
and memcombine canβt combine them. It doesnβt merge the four remaining loads
either, as they sit on top of the forwarded bytes.
Back in order
In summary, the rewriting rules run too early to be effective. A quick
workaround exists: run an earlier round of memcombine before late opt. After
this change, the generated code for the helper is back to the
shortest possible version:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
And the benchmark confirms it! βοΈ
goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
β Go 1.26.8 β Our branch β
β sec/op β sec/op vs base β
AddrTo6/safe 6.5470n Β± 0% 0.8944n Β± 4% -86.34% (p=0.002 n=6)
AddrTo6/unsafe 0.9071n Β± 3% 0.8682n Β± 1% -4.28% (p=0.002 n=6)
AddrTo6/builtin 0.8871n Β± 2% 0.8785n Β± 1% ~ (p=0.310 n=6)
Next steps
I think Go maintainers would reject this change because of the additional
memcombine pass. Instead, I plan to publish this blog post and bring up the
subject again as a follow-up to issue #54365. Either the sheer complexity and
the Goβ―1.27 regression convince the maintainers that adding a To6() method is
simpler and more efficient, or they advise me on how to move forward. Either
way, digging into this subject taught me a lot about the Go compiler! βοΈ
Update (2026-10)
I opened issue #81994 to propose Addr.To6(). Give it a π if you want it in Go!
-
IPv4-mapped IPv6 addresses let you handle only IPv6 addresses in your code and keep conversions at a few well-defined boundaries. I use them in Akvorado.Β β©
-
This is not strictly equivalent. The zero value becomes
::while it would make more sense to leave it untouched.Β β© -
I suppose Go maintainers keep it inconvenient on purpose: they donβt want random packages to patch the standard library or other packages.Β β©
-
To avoid catastrophic bugs when
netip.Addrβs layout changes, you must add tests to detect it. This is more dangerous if you put this code in a package. Either ask users to run the tests themselves, or detect the change at run time and panic. Hiding this optimization behind a build tag would make users aware of this potential trap.Β β© -
I produced the assembly code with
GOTOOLCHAIN=go1.26.8 GOAMD64=v3 ./go-asm '\.asm'from the companion repository, then edited it a bit to keep this article from turning into an endless rabbit hole. Due to an unfortunate sequence of events, we switch between versions: Goβ―1.27 introduced a regression that blurs the point of this article.Β β© -
The function is short enough for the compiler to inline it, so the assembly code may vary depending on the surrounding code.Β β©
-
MOVBEQrequiresGOAMD64=v3, matching the x86-64-v3 microarchitecture from 2013. Otherwise, the compiler translatesBEPutUint64()toBSWAPQ+MOVQandBEUint64()toMOVQ+BSWAPQ.Β β© -
When the call is used as a statement, a struct literal alone is invalid. The patch then turns it into
_ = netip.Addr{β¦}.Β β© -
The compiler can also write an HTML file with the result of each pass:
$ GOSSAFUNC=AddrTo6Safe \ > GOTOOLCHAIN=go1.26.8 \ > GOAMD64=v3 go build -a . dumped SSA for AddrTo6Safe,1 to ./ssa.html -
Source line numbers in parentheses follow value identifiers, but I removed them from the examples. The SSA form also features branches, but we donβt need them to understand our optimizations.Β β©
Vincent Bernat: Hacking the Go compiler to efficiently map IPv4 to IPv6
netip.Addr features an Unmap()
method returning the unwrapped IPv4 contained in an IPv4-mapped IPv6
address: from ::ffff:203.0.113.10 or ::ffff:cb00:710a, it returns
203.0.113.10.1 There is no Map() or To6() method for the reverse
direction. Such a method is trivial to implement, but Go maintainers have
rejected it on the grounds that users should write
netip.AddrFrom16(ip.As16()) and let the compiler optimize it.2 Today,
this pattern is eight times slower than a native method. How can we teach the
compiler to optimize this sequence?
The alternatives
Letβs explore three ways to implement the map semantics for netip.Addr. My
favorite is to add it to the Go standard library. Go maintainers prefer a small
external helper chaining netip.AddrFrom16() and netip.Addr.As16(), hoping
the compiler eventually optimizes it. The unsafe package
opens a third path, with the same performance as the first solution.
Modifying the Go standard library
Internally, netip.Addr stores any IP address as a
128-bit value with an extra field z to encode the family and the zone:
type Addr struct {
addr uint128
z unique.Handle[addrDetail]
}
type addrDetail struct {
isV6 bool // IPv4 is false, IPv6 is true.
zoneV6 string // != "" only if IsV6 is true.
}
var (
z0 unique.Handle[addrDetail]
z4 = unique.Make(addrDetail{})
z6noz = unique.Make(addrDetail{isV6: true})
)
AddrFrom4() encodes an IPv4 address as an
IPv4-mapped IPv6 address and sets z to the unique value z4:
// AddrFrom4 returns the address of the IPv4 address given by the bytes in addr.
func AddrFrom4(addr [4]byte) Addr {
return Addr{
addr: uint128{
0,
0xffff00000000 |
uint64(addr[0])<<24 | uint64(addr[1])<<16 |
uint64(addr[2])<<8 | uint64(addr[3])},
z: z4,
}
}
Unmap() turns an IPv4-mapped IPv6 address into an
IPv4 address by setting the z field to z4:
func (ip Addr) Unmap() Addr {
if ip.Is4In6() {
ip.z = z4
}
return ip
}
Implementing the reverse direction inside the Go standard library is trivial: we
set the z field to z6noz if the address is IPv4.
// To6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func (ip Addr) To6() Addr {
if ip.Is4() {
ip.z = z6noz
}
return ip
}
As a helper
We canβt access the z field from outside the net/netip package. Instead, we
build a small helper around the netip.AddrFrom16(ip.As16()) pattern:
// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func AddrTo6(ip netip.Addr) netip.Addr {
if ip.Is4() {
ip = netip.AddrFrom16(ip.As16())
}
return ip
}
As an unsafe function
Another solution uses the unsafe package to alter the Addr struct through
a proxy with the same memory layout:3
// addrProxy has the same memory layout as netip.Addr.
type addrProxy struct {
addr [2]uint64 // netip.uint128
z unsafe.Pointer // unique.Handle[netip.addrDetail]
}
var (
anyIPv6 = netip.IPv6Unspecified()
netipZ6noz = (*addrProxy)(unsafe.Pointer(&anyIPv6)).z
)
// AddrTo6 maps an IPv4 address to an IPv4-mapped IPv6 address. It returns an
// IPv6 address unmodified.
func AddrTo6(ip netip.Addr) netip.Addr {
if !ip.Is4() {
return ip
}
(*addrProxy)(unsafe.Pointer(&ip)).z = netipZ6noz
return ip
}
Benchmarks
On my computer, with Goβ―1.27.1, the standard library solution costs 0.88 ns per operation, while the solution favored by Go maintainers costs 7.14 ns. The unsafe solution matches the performance of the first one.
goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
β sec/op β
AddrTo6/safe 7.137n Β± 0%
AddrTo6/unsafe 0.8682n Β± 2%
AddrTo6/builtin 0.8775n Β± 2%
Assembly code
Letβs check the assembly code the compiler generates for each solution.4 The one built into the standard library looks like this:5
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
Goβs assembly language is not a direct representation of the underlying
machine language: it operates on a semi-abstract instruction set derived from
Planβ―9βs assembler. It has four pseudo-registers: FP (frame pointer
for function arguments), PC (program counter), SB (static base pointer for
global symbols), and SP (stack pointer). It also has architecture-specific
registers like AX, CX, DX, BX, SI, DI, and R8 to R15.
Instructions storing data use their last argument as the destination.
Instructions can carry an explicit size suffix: MOVB moves a byte, MOVWβ―16
bits, MOVLβ―32 bits, and MOVQβ―64 bits. In the example above, the first
instruction compares the 64-bit value z4 with the CX register.
The unsafe solution looks almost the same:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ netipZ6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The helper solution has far more instructions. To understand why, letβs look at
the code for As16() and
AddrFrom16(). They are short enough for the compiler
to inline them.
func (ip Addr) As16() (a16 [16]byte) {
byteorder.BEPutUint64(a16[:8], ip.addr.hi)
byteorder.BEPutUint64(a16[8:], ip.addr.lo)
return a16
}
func AddrFrom16(addr [16]byte) Addr {
return Addr{
addr: uint128{
byteorder.BEUint64(addr[:8]),
byteorder.BEUint64(addr[8:]),
},
z: z6noz,
}
}
We can already guess the pattern to optimize: the code packs the IP address into an array, copies it, then unpacks it. If we inline the Go code by hand, we get:
func AddrTo6(input netip.Addr) netip.Addr {
if !input.Is4() {
return input
}
var a16 [16]byte
byteorder.BEPutUint64(a16[:8], input.addr.hi)
byteorder.BEPutUint64(a16[8:], input.addr.lo)
addr := a16
var output netip.Addr
output.addr.hi = byteorder.BEUint64(addr[:8])
output.addr.lo = byteorder.BEUint64(addr[8:])
output.z = netip.z6noz
return output
}
As humans, we can mentally derive the optimized form:
func AddrTo6(input netip.Addr) netip.Addr {
if !input.Is4() {
return input
}
var output netip.Addr
output.addr.hi = input.addr.hi
output.addr.lo = input.addr.lo
output.z = netip.z6noz
return output
}
Unfortunately, as of Goβ―1.26.8, the compiler is not smart enough to do the same:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
; Push the stack (32 bytes):
; 0(SP) addr netip.uint128
; 16(SP) a16 [16]byte
PUSHQ BP
MOVQ SP, BP
SUBQ $32, SP
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)
; byteorder.BEPutUint64(a16[8:], input.addr.lo)
MOVBEQ AX, net/netipΒ·a16+16(SP)
MOVBEQ BX, net/netipΒ·a16+24(SP)
; addr = a16, 16 bytes at once through the vector register X0
MOVUPS net/netipΒ·a16+16(SP), X0
MOVUPS X0, net/netipΒ·addr(SP)
; CX = netip.z6noz
MOVQ net/netipΒ·z6noz(SB), CX
; Unpack: output.addr.hi = byteorder.BEUint64(addr[:8])
; output.addr.lo = byteorder.BEUint64(addr[8:])
MOVBEQ net/netipΒ·addr(SP), AX
MOVBEQ net/netipΒ·addr+8(SP), BX
end:
ADDQ $32, SP
POPQ BP
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The compiler does a decent job on the byte shuffling: the eight byte stores of
BEPutUint64() become a single MOVBEQ, which stores a register byte-swapped.
The eight byte loads of BEUint64() become a single MOVBEQ the other way
round.6 Three groups of instructions remain: a pack, a copy, and an
unpack.
Hacking the Go compiler
The Go compiler has several phases:
- Parsing
- The compiler tokenizes and parses the source code. It builds a syntax tree for each source file.
- Type checking
- The compiler maps each identifier to the object it denotes, folds constants, and infers the type of every expression.
- IR construction
- The compiler converts the syntax tree and its types into its own intermediate representation (IR). This process, called βnoding,β goes through a serialization format named unified IR.
- Middle end
- The compiler performs several optimization passes on the IR, such as devirtualization, function call inlining, and escape analysis.
- Walk
- This phase runs two steps: order of evaluation decomposes complex
statements into simpler ones, and desugaring transforms higher-level Go
constructs, like
switchor channels, into more primitive instructions or calls to the runtime. - Generic SSA
- The compiler converts the IR into Static Single Assignment (SSA) form, a lower-level intermediate representation suited for machine-independent optimizations and rewrite rules.
- Machine code generation
- The compiler rewrites the SSA form into machine-specific variants, allocates registers, and applies more optimization passes. At the end, the assembler turns the generated instructions into machine code.
The hammer
My first idea is to replace occurrences of netip.AddrFrom16(ip.As16()) with
netip.Addr{addr: ip.addr, z: netip.z6noz} as early as possible, during the
βnodingβ process. Before that, the type checking phase prevents
us from accessing unexported struct fields.
Goβ―1.27 introduced a convenient debug option to dump the IR of a function at interesting points during compilation:
$ GOTOOLCHAIN=go1.27.1 GOAMD64=v3 go build -a -gcflags="-d=astdump=AddrTo6Safe" .
Writing text ast output for AddrTo6Safe to AddrTo6Safe.ast
Writing html ast output for AddrTo6Safe to AddrTo6Safe.html
Writing html syntax output for AddrTo6Safe to AddrTo6Safe.syntax.html
In the HTML file, the first column shows the IR as it comes out of noding:
DCLFUNC addrto6.AddrTo6Safe ABI:ABIInternal FUNC-func(netip.Addr) netip.Addr
DCLFUNC-Dcl
. NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. NAME-addrto6.~r0 Class:PPARAMOUT Offset:0 OnStack netip.Addr
DCLFUNC-body
. IF # ipv6_safe.go:11:2
. IF-Cond
. . CALLFUNC bool
. . CALLFUNC-Fun
. . . METHEXPR addrto6.Is4 FUNC-func(netip.Addr) bool
. . . . TYPE netip.Addr Class:PEXTERN Offset:0 type netip.Addr
. . CALLFUNC-Args
. . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. IF-Body
. . AS # ipv6_safe.go:12:6
. . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . . CALLFUNC netip.Addr
. . . CALLFUNC-Fun
. . . . NAME-netip.AddrFrom16 Class:PFUNC Offset:0 Used FUNC-func([16]byte) netip.Addr
. . . CALLFUNC-Args
. . . . CALLFUNC ARRAY-[16]byte
. . . . CALLFUNC-Fun
. . . . . METHEXPR addrto6.As16 FUNC-func(netip.Addr) [16]byte
. . . . . . TYPE netip.Addr Class:PEXTERN Offset:0 type netip.Addr
. . . . CALLFUNC-Args
. . . . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. RETURN # ipv6_safe.go:14:2
. RETURN-Results
. . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
In the body of the if statement, we spot the calls to the method
netip.Addr.As16() and to the function netip.AddrFrom16(). Our goal is to
patch them with a struct literal:
IF-Body
. AS # ipv6_safe.go:12:6
. . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . STRUCTLIT netip.Addr
. . STRUCTLIT-List
. . . STRUCTKEY netip.addr
. . . . DOT netip.addr netip.uint128
. . . . . NAME-addrto6.ip Class:PPARAM Offset:0 OnStack Used netip.Addr
. . . STRUCTKEY netip.z
. . . . NAME-netip.z6noz Class:PEXTERN Offset:0 unique.Handle[net/netip.addrDetail]
In noderβs reader.go, the expr() method builds the IR tree for
an expression. At the end of the exprCall case, we add a call to a
rewriteAddrFrom16As16() function. It takes the current node and returns the
struct literal on success, or nil if the rewrite is not possible. First, we
check that we have the expected pattern: a call to the netip.AddrFrom16()
function with a call to the netip.Addr.As16() method as its only argument:
func rewriteAddrFrom16As16(n ir.Node) ir.Node {
call, ok := n.(*ir.CallExpr)
if !ok || call.Op() != ir.OCALLFUNC ||
len(call.Args) != 1 || len(call.Init()) != 0 ||
!isNetipFunc(call.Fun, "AddrFrom16") {
return nil
}
inner, ok := call.Args[0].(*ir.CallExpr)
if !ok || inner.Op() != ir.OCALLFUNC ||
len(inner.Args) != 1 || len(inner.Init()) != 0 ||
!isNetipFunc(inner.Fun, "Addr.As16") {
return nil
}
x := inner.Args[0]
// [...]
}
Then, we fetch netip.z6noz:
z6noz, err := lookupVar(ir.StaticCalleeName(call.Fun).Sym().Pkg, "z6noz")
if err != nil {
return nil
}
And we build the struct literal:
typ := call.Type()
pos := call.Pos()
var list []ir.Node
for i, f := range typ.Fields() {
var value ir.Node
switch f.Sym.Name {
case "addr":
value = typecheck.DotField(pos, x, i)
case "z":
value = z6noz
default:
return nil
}
list = append(list, ir.NewStructKeyExpr(pos, f, value))
}
lit := ir.NewCompLitExpr(pos, ir.OSTRUCTLIT, typ, list)
lit.SetTypecheck(1)
return lit
Have a look at the complete patch.7 We can test it with the following commands:
$ cd src
$ ./make.bash
Building Go cmd/dist using /usr/lib/go-1.27. (go1.27.1 linux/amd64)
Building Go toolchain1 and bootstrap cmd/go (go_bootstrap) using /usr/lib/go-1.27.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 and commands using go_bootstrap and Go toolchain2.
Checking command staleness for linux/amd64.
---
Installed Go for linux/amd64 in /home/bernat/code/free/go
Installed commands in /home/bernat/code/free/go/bin
*** You need to add /home/bernat/code/free/go/bin to your PATH.
$ export PATH=$PWD/../bin:$PATH
$ go version
go version go1.28-devel_9834516e20 Sat Sep 12 08:23:11 2026 -0700 linux/amd64
$ go test net/netip/...
ok net/netip 0.224s
$ cd ../../go-netip-addrto6
$ go test .
ok github.com/vincentbernat/go-netip-addrto6 0.062s
The generated code for the helper is now the shortest possible version!
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
Go maintainers are unlikely to accept this change. It relies on the internal
structure of net/netip.Addr. Itβs an ugly hack in the noder, whose job is to
faithfully translate the type-checked AST into the IR. And itβs harder to
maintain than adding a To6() method.
The screwdriver
The right place for such an optimization is the generic SSA phase. One of
the last machine-independent passes is memcombine. With the appropriate debug
flag, the compiler dumps the SSA form after this pass:8
$ GOTOOLCHAIN=go1.26.8 GOAMD64=v3 \
> go build -a -gcflags='-d=ssa/memcombine/dump=AddrTo6Safe' .
$ head -5 AddrTo6Safe_01__memcombine.dump
AddrTo6Safe func(netip.Addr) netip.Addr
b2:
(?) v1 = InitMem <mem>
(?) v2 = SP <uintptr>
(?) v3 = SB <uintptr>
The result of the memcombine pass follows the same structure as the assembly
code for AddrTo6Safe() we looked at earlier: two stores,
one move, and two loads we would like to optimize away.
; [β¦]
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v466 = ArgIntReg <*netip.addrDetail> {ip+16} [2] ; input.z
; [β¦]
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo)
v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi)
v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v415 = OffPtr <*byte> [8] v285 ; &addr[8]
v416 = Load <uint64> v285 v286 ; addr[:8]
v299 = Bswap64 <uint64> v416 ; output.addr.hi
v174 = Load <uint64> v415 v286 ; addr[8:]
v39 = Bswap64 <uint64> v174 ; output.addr.lo
; [β¦]
Each line features a value identifier (v442), an operation with its type
(Bswap64 <uint64>), and its arguments (v490).9 Values are the basic
building blocks of SSA and are defined exactly once. Square brackets enclose
integer parameters ([8]) and curly braces contain auxiliary arguments
({netip.addr}). Operations writing to memory produce a new memory state. Every
memory operation takes the current state as its last argument, which keeps them
in order.
On paper
Letβs focus on output.addr.lo, aka v39:
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v415 = OffPtr <*byte> [8] v285 ; &addr[8]
v174 = Load <uint64> v415 v286 ; addr[8:]
v39 = Bswap64 <uint64> v174 ; output.addr.lo
To simplify this code, we could apply three rewriting rules:
-
The first one adds a shortcut when loading through a move:
(Load (OffPtr [o] p) (Move p src mem)) => (Load (OffPtr [o] src) mem). This matchesv174with its argumentsv415andv286and creates a new valuev600:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Load <uint64> v600 v282 ; a16[8:] v39 = Bswap64 <uint64> v174 ; output.addr.lo -
The second one simplifies a load following a store:
(Load p (Store p x _)) => x. The load is forwarded: the stored value replaces it and no memory access remains. It matchesv174. It notices thatv600andv173are the same address and replacesv174with a copy ofv442:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Copy <uint64> v442 ; bswap(input.addr.lo) v39 = Bswap64 <uint64> v174 ; output.addr.lo -
The last step cancels the two byte swaps:
(Bswap64 (Bswap64 x)) => x.v39becomes a copy ofv490:v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v442 = Bswap64 <uint64> v490 ; bswap(input.addr.lo) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v415 = OffPtr <*byte> [8] v285 ; &addr[8] v600 = OffPtr <*byte> [8] v22 ; &a16[8] v174 = Copy <uint64> v442 ; bswap(input.addr.lo) v39 = Copy <uint64> v490 ; output.addr.lo = input.addr.lo
If we ignore the values not needed to compute v39, only this SSA form remains:
v490 = ArgIntReg <uint64> {ip+8} [1] ; input.addr.lo
v39 = Copy <uint64> v490 ; output.addr.lo = input.addr.lo
Letβs switch to output.addr.hi, aka v299:
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16
v173 = OffPtr <*byte> [8] v22 ; &a16[8]
v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi)
v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi)
v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
v416 = Load <uint64> v285 v286 ; addr[:8]
v299 = Bswap64 <uint64> v416 ; output.addr.hi
To optimize it away, we also apply three rewriting rules:
-
The first one also adds a shortcut when loading through a move, but without an offset:
(Load p (Move p src mem)) => (Load src mem). This rewritesv416to use arguments fromv286:v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Load <uint64> v22 v282 ; a16[:8] v299 = Bswap64 <uint64> v416 ; output.addr.hi -
The second rule forwards a value stored one step earlier, skipping over a store to another address:
(Load p (Store q _ (Store p x _))) => x. This matchesv416:xisv542,pisv22(&a16),qisv173(&a16[8]), andpandqdo not overlap foruint64.v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Copy <uint64> v542 ; bswap(input.addr.hi) v299 = Bswap64 <uint64> v416 ; output.addr.hi -
The third rule cancels two byte swaps:
(Bswap64 (Bswap64 x)) => x.v299becomes a copy ofv502:v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi v22 = LocalAddr <*[16]byte> {netip.a16} v2 v1 ; &a16 v173 = OffPtr <*byte> [8] v22 ; &a16[8] v542 = Bswap64 <uint64> v502 ; bswap(input.addr.hi) v161 = Store <mem> {uint64} v22 v542 v23 ; a16[:8] = bswap(hi) v282 = Store <mem> {uint64} v173 v442 v161 ; a16[8:] = bswap(lo) v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16 v416 = Copy <uint64> v542 ; bswap(input.addr.hi) v299 = Copy <uint64> v502 ; output.addr.hi = input.addr.hi
If we remove the values not used to compute v299, we get this SSA form:
v502 = ArgIntReg <uint64> {ip+0} [0] ; input.addr.hi
v299 = Copy <uint64> v502 ; output.addr.hi = input.addr.hi
In practice
Most of these rules already exist in generic.rules. They use
conditions to validate their context: ssa.IsSamePtr() for the same address,
ssa.Disjoint() for addresses that do not overlap. The rule forwarding a stored
value to a load already exists with three variants looking through several other
stores. Here are the two we need:
(Load <t1> p1 (Store {t2} p2 x _))
&& ssa.IsSamePtr(p1, p2)
&& copyCompatibleType(t1, x.Type)
&& t1.Size() == t2.Size()
=> x
(Load <t1> p1 (Store {t2} p2 _ (Store {t3} p3 x _)))
&& ssa.IsSamePtr(p1, p3)
&& copyCompatibleType(t1, x.Type)
&& t1.Size() == t3.Size()
&& ssa.Disjoint(p3, t3, p2, t2)
=> x
Goβ―1.27 added the rule loading through a move with CLβ―748200 to fix issueΒ #77720:
(Load <t1> op1:(OffPtr [o1] p1) move:(Move [n] p2 src mem))
&& o1 >= 0 && o1+t1.Size() <= n && ssa.IsSamePtr(p1, p2)
&& !ssa.IsVolatile(src)
=> @move.Block (Load <t1> (OffPtr <op1.Type> [o1] src) mem)
It lacks a variant without an offset:
(Load <t1> p1 move:(Move [n] p2 src mem))
&& p1.Op != ssaop.OpOffPtr
&& t1.Size() <= n && ssa.IsSamePtr(p1, p2)
&& !ssa.IsVolatile(src)
=> @move.Block (Load <t1> (OffPtr <p1.Type> [0] src) mem)
There is no generic rule to cancel two byte swaps, but the AMD64 lowering pass includes this rule:
(BSWAP(Q|L) (BSWAP(Q|L) p)) => p
After switching to Goβs development branch and adding the missing rule, the generated assembly code is worse than with Goβ―1.26.8, even though our additional rule slightly improves the situation at the end:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
; Push the stack (16 bytes):
; 0(SP) a16 [16]byte
PUSHQ BP
MOVQ SP, BP
SUBQ $16, SP
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
; The four forwarded bytes: the low half of input.addr.lo is taken apart and
; put back together in registers
MOVQ BX, DX ; DX = input.addr.lo
SHRQ $24, BX ; BX = input.addr.lo >> 24
MOVQ DX, SI ; SI = input.addr.lo
SHRQ $16, DX ; DX = input.addr.lo >> 16
MOVQ SI, DI ; DI = input.addr.lo, kept for the pack
SHRQ $8, SI ; SI = input.addr.lo >> 8
MOVBLZX DIB, R8 ; R8 = byte(input.addr.lo)
MOVBLZX SIB, SI ; SI = byte(input.addr.lo >> 8)
SHLQ $8, SI
ORQ R8, SI ; SI = two low bytes of input.addr.lo
MOVBLZX DL, DX ; DX = byte(input.addr.lo >> 16)
SHLQ $16, DX
ORQ SI, DX
MOVBLZX BL, BX ; BX = byte(input.addr.lo >> 24)
SHLQ $24, BX
ORQ DX, BX ; BX = input.addr.lo & 0xffffffff
; Pack: byteorder.BEPutUint64(a16[:8], input.addr.hi)
; byteorder.BEPutUint64(a16[8:], input.addr.lo)
MOVBEQ AX, net/netipΒ·a16(SP)
MOVBEQ DI, net/netipΒ·a16+8(SP)
; The four other bytes of input.addr.lo, read one by one from a16
MOVBLZX net/netipΒ·a16+11(SP), DX ; a16[11]
SHLQ $32, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+10(SP), DX ; a16[10]
SHLQ $40, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+9(SP), DX ; a16[9]
SHLQ $48, DX
ORQ DX, BX
MOVBLZX net/netipΒ·a16+8(SP), DX ; a16[8]
SHLQ $56, DX
; output.z = netip.z6noz
MOVQ net/netipΒ·z6noz(SB), CX
; output.addr.hi = byteorder.BEUint64(a16[:8])
MOVBEQ net/netipΒ·a16(SP), AX
; output.addr.lo assembled from the previous steps
ORQ DX, BX
end:
LEAVEQ
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
The rule loading through a move, added in Goβ―1.27, introduced this regression.
Out of order
Letβs not give up now! In reality, the rewriting rules run before memcombine,
notably in the late opt pass. At this point, the inlined versions of
BEPutUint64() and BEUint64() still expand to sixteen byte stores and sixteen
byte loads, matching their source code:
func BEUint64(b []byte) uint64 {
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
}
Letβs follow two bytes of output.addr.lo: addr[15] and addr[11]. Here is
a simplified SSA form before late opt:
v273 = Trunc64to8 <byte> v490 ; byte(input.addr.lo)
v226 = Trunc64to8 <byte> v225 ; byte(input.addr.lo >> 32)
; [β¦]
v235 = Store <mem> {byte} v233 v226 v223 ; a16[11] = byte(lo >> 32)
v247 = Store <mem> {byte} v245 v238 v235 ; a16[12] = β¦
v259 = Store <mem> {byte} v257 v250 v247 ; a16[13] = β¦
v271 = Store <mem> {byte} v269 v262 v259 ; a16[14] = β¦
v282 = Store <mem> {byte} v280 v273 v271 ; a16[15] = byte(lo)
v285 = LocalAddr <*[16]byte> {netip.addr} v2 v282 ; &addr
v286 = Move <mem> {[16]byte} [16] v285 v22 v282 ; addr = a16
; [β¦]
v433 = OffPtr <*byte> [15] v285 ; &addr[15]
v435 = Load <byte> v433 v286 ; addr[15]
v479 = OffPtr <*byte> [11] v285 ; &addr[11]
v481 = Load <byte> v479 v286 ; addr[11]
The first rule loads through the move: (Load (OffPtr [o] p) (Move p src mem))
=> (Load (OffPtr [o] src) mem). It matches both loads, which now read a16
with the memory state before the copy:
v600 = OffPtr <*byte> [15] v22 ; &a16[15]
v435 = Load <byte> v600 v282 ; a16[15]
v601 = OffPtr <*byte> [11] v22 ; &a16[11]
v481 = Load <byte> v601 v282 ; a16[11]
The second rule shortcuts a load following a store: (Load p (Store p x _)) =>
x. It matches v435, as v282 stores a16[15]. It does not match v481:
v235 stores a16[11] four stores earlier in the chain, while the variants of
this rule look through three stores at most.
v435 = Copy <byte> v273 ; byte(input.addr.lo)
v601 = OffPtr <*byte> [11] v22 ; &a16[11]
v481 = Load <byte> v601 v282 ; a16[11]
The same happens to the other bytes: the rule forwards the four bytes stored
last, a16[12] to a16[15]. The twelve other loads now read a16 instead of
addr.
BEUint64() becomes a chain of Or64, each one adding a byte shifted into
place. memcombine is a pass written in Go, not a set of rewrite
rules. It starts from the last Or64 of the chain and collects up to eight
terms. If each term is a byte load, extended to 64 bits and shifted, and if the
eight loads read consecutive addresses from the same pointer with the same
memory state, it replaces the whole chain with a single 64-bit load and a byte
swap. Otherwise, it tries again with four, then two terms, and from each
intermediate Or64. Here is the loop checking each term in a simplified version
of combineLoads():
for i := int64(0); i < n; i++ {
v := a[i]
shift := int64(0)
if v.Op == shiftOp {
v, shift = peelShift(v)
}
if v.Op != extOp {
return false
}
load := v.Args[0]
if load.Op != ssaop.OpLoad {
return false
}
if load.Args[1] != mem {
return false
}
p, off := splitPtr(load.Args[0])
if p != base {
return false
}
r[i] = LoadRecord{load: load, offset: off, shift: shift}
}
For output.addr.hi, the eight loads read a16 with the same memory state
v282:
v13 = Load <byte> v22 v282 ; a16[0]
v530 = Load <byte> v14 v282 ; a16[1]
v488 = Load <byte> v504 v282 ; a16[2]
v405 = Load <byte> v537 v282 ; a16[3]
v385 = Load <byte> v397 v282 ; a16[4]
v361 = Load <byte> v373 v282 ; a16[5]
v196 = Load <byte> v63 v282 ; a16[6]
v432 = Load <byte> v315 v282 ; a16[7]
v319 = ZeroExt8to64 <uint64> v432 ; uint64(a16[7])
v329 = ZeroExt8to64 <uint64> v196 ; uint64(a16[6])
v330 = Lsh64x64 <uint64> [true] v329 v138 ; uint64(a16[6]) << 8
v331 = Or64 <uint64> v319 v330 ; a16[7] | a16[6] << 8
; [β¦] same for a16[5] to a16[1]
v401 = ZeroExt8to64 <uint64> v13 ; uint64(a16[0])
v402 = Lsh64x64 <uint64> [true] v401 v55 ; uint64(a16[0]) << 56
v403 = Or64 <uint64> v402 v391 ; | a16[0] << 56 = output.addr.hi
memcombine merges them into one load and a swap:
v286 = Load <uint64> v22 v282 ; a16[:8]
v285 = Bswap64 <uint64> v286 ; output.addr.hi
For output.addr.lo, here is the chain memcombine sees after late opt:
v436 = ZeroExt8to64 <uint64> v273 ; addr[15], forwarded
v448 = Or64 <uint64> v436 v447 ; | addr[14] << 8, forwarded
v460 = Or64 <uint64> v459 v448 ; | addr[13] << 16, forwarded
v472 = Or64 <uint64> v471 v460 ; | addr[12] << 24, forwarded
v484 = Or64 <uint64> v483 v472 ; | a16[11] << 32, loaded
v496 = Or64 <uint64> v495 v484 ; | a16[10] << 40, loaded
v508 = Or64 <uint64> v507 v496 ; | a16[9] << 48, loaded
v520 = Or64 <uint64> v519 v508 ; | a16[8] << 56, loaded
From v520, four of the eight terms are forwarded bytes, not loads from memory,
and memcombine canβt combine them. It doesnβt merge the four remaining loads
either, as they sit on top of the forwarded bytes.
Back in order
In summary, the rewriting rules run too early to be effective. A quick
workaround exists: run an earlier round of memcombine before late opt. After
this change, the generated code for the helper is back to the
shortest possible version:
// AX = input.addr.hi, BX = input.addr.lo, CX = input.z
CMPQ net/netipΒ·z4(SB), CX ; check "z" if this is an IPv4 address
JNE end ; if not, stop here
MOVQ net/netipΒ·z6noz(SB), CX ; CX = netip.z6noz
end:
RET
// return value = Addr{hi: AX, lo: BX, z: CX}
And the benchmark confirms it! βοΈ
goos: linux
goarch: amd64
pkg: github.com/vincentbernat/go-netip-addrto6
cpu: AMD Ryzen 5 5600X 6-Core Processor
β Go 1.26.8 β Our branch β
β sec/op β sec/op vs base β
AddrTo6/safe 6.5470n Β± 0% 0.8944n Β± 4% -86.34% (p=0.002 n=6)
AddrTo6/unsafe 0.9071n Β± 3% 0.8682n Β± 1% -4.28% (p=0.002 n=6)
AddrTo6/builtin 0.8871n Β± 2% 0.8785n Β± 1% ~ (p=0.310 n=6)
Next steps
I think Go maintainers would reject this change because of the additional
memcombine pass. Instead, I plan to publish this blog post and bring up the
subject again as a follow-up to issue #54365. Either the sheer complexity and
the Goβ―1.27 regression convince the maintainers that adding a To6() method is
simpler and more efficient, or they advise me on how to move forward. Either
way, digging into this subject taught me a lot about the Go compiler! βοΈ
-
IPv4-mapped IPv6 addresses let you handle only IPv6 addresses in your code and keep conversions at a few well-defined boundaries. I use them in Akvorado.Β β©
-
This is not strictly equivalent. The zero value becomes
::while it would make more sense to leave it untouched.Β β© -
To avoid catastrophic bugs when
netip.Addrβs layout changes, you must add tests to detect it. This is more dangerous if you put this code in a package. Either ask users to run the tests themselves, or detect the change at run time and panic. Hiding this optimization behind a build tag would make users aware of this potential trap.Β β© -
I produced the assembly code with
GOTOOLCHAIN=go1.26.8 GOAMD64=v3 ./go-asm '\.asm'from the companion repository, then edited it a bit to keep this article from turning into an endless rabbit hole. Due to an unfortunate sequence of events, we switch between versions: Goβ―1.27 introduced a regression that blurs the point of this article.Β β© -
The function is short enough for the compiler to inline it, so the assembly code may vary depending on the surrounding code.Β β©
-
MOVBEQrequiresGOAMD64=v3, matching the x86-64-v3 microarchitecture from 2013. Otherwise, the compiler translatesBEPutUint64()toBSWAPQ+MOVQandBEUint64()toMOVQ+BSWAPQ.Β β© -
When the call is used as a statement, a struct literal alone is invalid. The patch then turns it into
_ = netip.Addr{β¦}.Β β© -
The compiler can also write an HTML file with the result of each pass:
$ GOSSAFUNC=AddrTo6Safe \ > GOTOOLCHAIN=go1.26.8 \ > GOAMD64=v3 go build -a . dumped SSA for AddrTo6Safe,1 to ./ssa.html -
Source line numbers in parentheses follow value identifiers, but I removed them from the examples. The SSA form also features branches, but we donβt need them to understand our optimizations.Β β©
Steinar H. Gunderson: Decompilation patterns part 1: Anti-CSE
There's a lot ofΒΉ interest in decompilation these days; taking some old piece of software (usually a game) and try to reconstruct C that does the same thing. (This is often for modding purposes, but also often for understanding, or just as a personal challenge.) An often sought-after property is matching; that the decompiled C produces 100% the same bytes as the original after compilation. (This means you'll need to find the original compiler and build flags, of course.) Typically, one starts with the disassembled code and runs m2c on it, which then spits out some C that is fairly close to the original, but rarely produces a match without further cleanup. (It is usually much closer to a match than what Ghidra or Hex-Rays would output; Ghidra is made for ease of understanding, not matching, so it tends to simplify a lot.)
Cleaning up the code for matching has two main advantages:
- It usually makes the source easier to understand, and
- Matching is a very strong argument (though not infallible) that the decompilation is actually correct.
Like with pretty much any other field these days, there is a lot of interest in giving this task to some LLM, which means there's now a lot of decompilations around that match (fitting #2 perfectly) and completely fail #1. :-) Anyway, at some point, I learned that there is almost no public documentation on the cleaning-up-for-matching part; it's all folklore and people rediscovering the same patterns. So I thought I'd do something about thatΒ².
My general goal is to write up one decompilation pattern a day (focusing on what I know best, namely GCC on MIPS), with some example. Most of them will be very simple; some are much less obvious. I'll keep going until I run out of ideas. None are formally named, so I'll just invent a name. So here goes part one:
temp_v0_5 = g_state.players[1].unk2;
if ((temp_v0_5 != 7) && (temp_v0_5 != 0)) {
What happened here is usually that the compiler has done common subexpression elimination; someone wrote the same thing twice and then the compiler decided to just calculate it once and put it into a temporary variable. (Or the programmer did, in which case you would be wise to leave it alone! Getting into the original programmer's head is part of the challenge.) This will often affect register allocation and/or stack layout, so to get a better match, you could try doing CSE in reverse:
if (g_state.players[1].unk2 != 7 && g_state.players[1].unk2 != 0) {
Nothing is mechanical; it may help or it may not. (It may also appear to hurt at first and then help later, or vice versa.) But anti-CSE is generally very common and you'll want to have it in your arsenal.
ΒΉ Well, all is relative.
Β² Reluctantly, with the understanding that the LLMs might also learn from it.
Distribution Release: Parrot 7.4
-
MacRumors
- Top Stories: Apple Smart Home Launch on October 13, iPhone Duo Production Issues, and More
Top Stories: Apple Smart Home Launch on October 13, iPhone Duo Production Issues, and More
October 13 looks to be the date for an introduction of Apple's revamped smart home portfolio, including the home hub device we've been hearing about for ages. This week also saw word that Apple's supply chain is struggling to meet production yield goals for the iPhone Duo ahead of its launch, while iPhone 18 Pro owners are now getting a chance to put the devices through their paces, so read on below for all of the details!
Top Stories
Three New Apple Smart Home Products Coming on October 13
A couple of those leaked products above could be arriving very shortly, as Bloomberg's Mark Gurman says Apple will be introducing a trio of new smart home products on Tuesday, October 13.
Apple will reportedly debut its long-rumored smart home hub product, as well as the new HomePod mini and Apple TV models, and while he didn't specify how Apple will introduce them, signs are pointing to some sort of event.
Apple retail stores have reportedly also received confidential boxes that are marked "Do Not Open Until October 8" and which are likely to contain merchandising materials. It's unclear, however, if they are for the upcoming iPhone Duo launch or something related to the smart home product introduction.
Apple Smart Home Hub to Feature iMac G4-Style Design
Apple's long-rumored smart home hub will have an iMac G4-style design and a host of unique features, Bloomberg's Mark Gurman reports.
The smart home hub is said to feature a square 6-inch display, with variants that can be placed on a countertop or mounted on a wall. It features a single FaceTime camera on the front, with microphones and speakers in its connected base.
Leaks suggest the hub may be available in Silver, Space Gray, Starlight, and Rose Pink colors, and will be able to display user-customizable Photo Faces pulled from your photos library, memories, and shared albums, a feature that appears similar to what will be available for StandBy mode on the iPhone Duo.
iPhone Duo Reportedly Facing Production Problems Ahead of Launch
The iPhone Duo is reportedly facing production difficulties ahead of its October launch, with early output expected to be limited.
A person said to be close to Foxconn's supply chain told China's Jiemian News that final assembly yields for Apple's first foldable iPhone were only slightly above 60% as of September 17. The source said that, under Apple's current quality standards, it could take another six months to a year for the assembly line to reach a mature yield.
According to the source, Foxconn is repeatedly adjusting equipment, assembly processes, and production settings as it makes the iPhone Duo at scale. The source also said suppliers were waiting to see whether Apple would ease any quality requirements to accelerate production.
iPhone 18 Pro First Impressions: What Do You Think So Far?
It's been almost two weeks since the iPhone 18 Pro models launched, and we want to hear from MacRumors readers. What do you think of Apple's newest iPhones? If you upgraded from an older phone, was it worth it? What about upgrading from an iPhone 17 Pro? What problems are you running into? Anything you love so far?
MacRumors readers have already been sharing early opinions on the MacRumors forums, and there are interesting discussion threads to read through. For early reviews and hands-on images, make sure to check out the first impressions and photos thread.
Not everything has been perfect, however, as some users have been experiencing issues such as speaker crackling or popping while some iPhone 18 Pro Max users on AT&T in the U.S. have been having issues with cellular connectivity or eSIM provisioning.
Apple Planning to Launch Products Faster and More Often
John Ternus is marking one month as Apple CEO, and he is apparently undertaking efforts to restructure the company to accelerate product development and introduce new products more frequently, according to Bloomberg's Mark Gurman.
Ternus apparently wants Apple to "broaden its range of devices and create a leaner organization with a greater focus on engineering." One proposal under consideration is to rely less on Apple's traditional spring and fall product release cycles, and introduce new devices more quickly and throughout the year. Ternus has also purportedly indicated that Apple must become "more experimental" to remain competitive in the era of artificial intelligence.
MacRumors Newsletter
Each week, we publish an email newsletter like this highlighting the top Apple stories, making it a great way to get a bite-sized recap of the week hitting all of the major topics we've covered and tying together related stories for a big-picture view.
So if you want to have top stories like the above recap delivered to your email inbox each week, subscribe to our newsletter!
This article, "Top Stories: Apple Smart Home Launch on October 13, iPhone Duo Production Issues, and More" first appeared on MacRumors.com
Discuss this article in our forums
Vincent Bernat: Self-hosted HTTP tunnels with SSH and nginx
A friend wants to proofread your work-in-progress blog post, but its preview
only runs on localhost:8080. Several tools can help. Some run as a
commercial service, like ngrok or Cloudflare Quick Tunnels. Some are
self-hostable but require a specific client, like frp or localtunnel.
Some only require a plain SSH client but rely on a specific SSH server, like
sish. Letβs implement a self-hosted solution with only OpenSSH and
nginx!
$ ssh -R 0:localhost:8080 http-over-ssh
Allocated port 41535 for remote forward to localhost:8080
https://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/
Basic setup
First, we forward connections from a port on a remote server to your local service:
$ ssh -N -R 0:localhost:8080 web02.luffy.cx
Allocated port 41535 for remote forward to localhost:8080
When you specify 0 as the remote port, the server allocates a free port.
Then, we configure nginx to proxy requests from https://p41535.ssh.luffy.cx to
http://127.0.0.1:41535:
server {
listen 0.0.0.0:443 ssl ;
listen [::0]:443 ssl ;
server_name ~^p(?<port>\d\d\d\d\d)\.ssh\.luffy\.cx$;
location / {
proxy_pass http://127.0.0.1:$port;
}
}
We also need to add DNS records for *.ssh.luffy.cx and get a wildcard
certificate through Letβs Encrypt:
*.ssh.luffy.cx. CNAME web02.luffy.cx.
ssh.luffy.cx. CAA 0 issuewild "letsencrypt.org"
_acme-challenge.ssh.luffy.cx CNAME ssh.luffy.cx.acme.luffy.cx.
acme.luffy.cx is a zone hosted on Routeβ―53. I use it for ACME DNS-01
challenges, both for wildcard certificates and for domains served by
several web servers. In my case, NixOS gets the certificates
automatically.
Access control
The port is the only βsecretβ1 keeping the content confidential. Other forwarding solutions add a random string to the domain name to prevent an intruder from enumerating the possible values.
Thanks to ngx_http_secure_link_module, we can secure
this setup a bit. This module computes a hash2 over a set of values,
including a secret, and compares it with the hash from the request. The hash is
base64-encoded, so we cannot put it in the domain name, which is
case-insensitive. Instead, we put it in the URL as a username, along with its
expiration timestamp:3
https://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/en/blog
β°ββββββββββ¬βββββββββββ― β°ββββ¬βββββ― β°ββ¬ββ― β°βββ¬ββββ―
hash expires port path
The client sends the username to the server with HTTP basic
authentication. This works with most HTTP clients, including curl.
Nginx exposes the username in the $remote_user variable. The module expects
the hash and the expiration timestamp separated by a comma. We use a map
directive to extract the two parts from $remote_user and join them with a
comma.4 We also give the module the string to hash. It contains the
expiration timestamp, the port, and a secret:
map $remote_user $httpssh_link {
"~^([-_A-Za-z0-9]{22})--([0-9]+)$" "$1,$2";
}
server {
# [β¦]
location / {
secure_link $httpssh_link;
secure_link_md5 "$secure_link_expires $port ZuPerS3cr3!";
}
}
The module returns the status of the check in the $secure_link variable:
- empty if the hashes do not match,
"0"if they match but the link has expired, or"1"otherwise.
If the hash is incorrect or missing, we return a 401 error with a
WWW-Authenticate header to ask for credentials. If the link has expired, we
return a 410 error. We remove the Authorization header before forwarding the
request and add a few directives to proxy WebSocket connections.
Here is the complete configuration:5
map $remote_user $httpssh_link {
"~^([-_A-Za-z0-9]{22})--([0-9]+)$" "$1,$2";
}
server {
listen 0.0.0.0:443 ssl ;
listen [::0]:443 ssl ;
server_name ~^p(?<port>\d\d\d\d\d)\.ssh\.luffy\.cx$;
location / {
secure_link $httpssh_link;
secure_link_md5 "$secure_link_expires $port ZuPerS3cr3!";
if ($secure_link = "") {
add_header WWW-Authenticate 'Basic realm="tunnel"' always;
return 401;
}
if ($secure_link = "0") {
return 410;
}
proxy_pass http://127.0.0.1:$port;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization "";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 30m;
}
}
I think you are now asking yourself the obvious question: βHow should I generate the hash?β Easy peasy!
$ expires=$(( $(date +%s) + 86400 ))
$ port=41535
$ secret='ZuPerS3cr3!'
$ printf '%s %s %s' "$expires" "$port" "$secret" \
> | openssl md5 -binary \
> | openssl base64 \
> | tr +/ -_ | tr -d =
6J3jK1WmB15c6WmjW_X-Wg
Well, I suppose you are now saying: βVincent, this is not very convenient! Iβll stick with ngrok if you donβt mind.β Okay, I hear you. Letβs write a helper script.
Helper script
The main difficulty is finding the ephemeral port that OpenSSH allocates, as it
does not appear in any environment variable.6 To work around this obstacle,
we look for the ancestor sshd-session processes:7
pids=$(
pid=$$
while [ "$pid" -gt 1 ]; do
line=$(ps -o comm=,pid=,ppid= -p "$pid")
echo "$line"
pid=${line##* }
done | awk '$1 == "sshd-session" { printf "pid=%s,\n", $2 }'
)
if [ -z "$pids" ]; then
echo "not an ssh session" >&2
exit 1
fi
Then, we get the listening ports associated with these sshd-session
processes:8
ports=$(sudo -n ss --listening --numeric --tcp --processes --no-header \
| grep -F "$pids" \
| awk '{ print $4 }' | awk -F: '{ print $NF }' \
| sort -un)
if [ -z "$ports" ]; then
echo "no forwarded port, use ssh -R 0:localhost:PORT" >&2
exit 1
fi
Finally, we display the URLs and keep the session open:
lifetime=86400
secret='ZuPerS3cr3!'
expires=$(( $(date +%s) + lifetime ))
for port in $ports; do
token=$(printf '%s %s %s' "$expires" "$port" "$secret" \
| openssl md5 -binary \
| openssl base64 \
| tr +/ -_ | tr -d =)
echo "https://$token--$expires@p$port.ssh.luffy.cx/"
done
sleep infinity
I install this script as http-over-ssh on the server and add this entry to my
~/.ssh/config:
Host http-over-ssh
Hostname web02.luffy.cx
RemoteCommand http-over-ssh
ControlPath none
With this solution, I only rely on OpenSSH and nginx, two pieces of software
already running on this server. One short command gives me a self-hosted tunnel
and a URL to share. To try it, grab the complete helper script, which
includes a few minor improvements. If you run NixOS, as any person of taste
would, have a look at my http-over-ssh.nix instead. βοΈ
Update (2026-10)
This post was widely discussed on Hacker News.
Update (2026-10)
I have since switched to Caddy, which does not include a
module to check the validity of a URL. Instead, Caddy queries a local HTTP
service written in Go through the forward_auth directive. As a bonus,
this lets me put the token in the domain name.
-
Since the kernel allocates it from the local port range, its entropy is low. Moreover, on Linux, we lose one more bit because the kernel favors odd ports when it picks a random free port.
$ sysctl -n net.ipv4.ip_local_port_range \ > | awk '{print $1"β"$2" β "log($2-$1+1)/log(2)" bits"}' 32768β60999 β 14.785 bits -
The module relies on MD5. Its security is weak but good enough for this use.Β β©
-
We need an expiration date because a later session could reuse the port, and we have no way to detect this case.Β β©
-
The username could use a comma directly instead of a double dash. But some applications do not recognize the URL correctly, making it harder to share.Β β©
-
This configuration exposes any TCP port listening on
127.0.0.1or0.0.0.0to anyone with the secret, bypassing most firewall rules. You could strengthen it by limiting theserver_nameregex to the ephemeral port range.Β β© -
One SSH session can contain several tunnels, and the client can add or remove them at any time. This is unlike tun device forwarding (
ssh -w), which gets its ownSSH_TUNNELenvironment variable.Β β© -
Since OpenSSHβ―9.8,
sshd-sessionis the name of the helper handling the session. With older versions, look forsshdinstead.Β β© -
We need
sudobecause thesshd-sessionprocess dropped its privileges. The kernel then marks it as not dumpable, and its/proc/PID/fddirectory belongs to root. Without access to this directory,sscannot find which process owns a socket.Β β©
Apple Says iPhone 18 Pro Max Users With AT&T Cellular Issue Need to Have Device Replaced
Apple said it released iOS 27.0.1 earlier this week and an AT&T carrier settings update earlier today to help prevent more iPhone 18 Pro Max users from being impacted by this issue going forward. In a statement shared with MacRumors, Apple strongly encouraged all iPhone 18 Pro Max users to update to iOS 27.0.1 now. To install the update, open the Settings app on your iPhone and tap on General β Software Update.
The carrier settings update for the iPhone 18 Pro Max will automatically download for AT&T users over the next 14 days, according to Apple. To manually trigger the update, users can open the Settings app and tap on General β About.
Apple's full statement:
We have identified an issue affecting a small number of iPhone 18 Pro Max users on the AT&T network that may cause a device to lose service and be unable to make calls. We released iOS 27.0.1 earlier this week and strongly encourage all iPhone 18 Pro Max users to update now, and are also issuing a carrier settings update today. These updates together are meant to help prevent this issue from occurring.Notably, Apple said that iPhone 18 Pro Max devices that have been affected by this cellular issue on the AT&T network cannot have service reactivated with a software update. Instead, Apple said the device must be replaced entirely, and it advises affected customers to contact Apple Support or AT&T, or visit an Apple Store or AT&T location.
Apple said it has only observed this issue with iPhone 18 Pro Max users on the AT&T network. No other iPhone models or carriers are known to be impacted.
Apple did not disclose the underlying cause of the issue. U.S. models of the iPhone 18 Pro Max rely on Qualcomm's Snapdragon X80 modem for cellular connectivity, while variants of the device sold in all other countries are equipped with Apple's custom C2 modem. It is unclear if this modem difference is a factor in this AT&T issue.
It is also unclear if there will eventually be an Apple Service Program dedicated to this issue. In the meantime, it sounds like Apple Stores will replace affected iPhone 18 Pro Max devices for free, so book an appointment with the Genius Bar if necessary.
This article, "Apple Says iPhone 18 Pro Max Users With AT&T Cellular Issue Need to Have Device Replaced" first appeared on MacRumors.com
Discuss this article in our forums
Distribution Release: Qubes OS 4.3.2
Apple Announces 'Full Disk Access' Changes on macOS Due to AI Agents
"Some developers are using Full Disk Access in ways that could put users at risk, exposing everything on their systemsβincluding files, mail, messages, and even browsing historyβwithout users' full knowledge and understanding," said Apple.
"As AI agents become increasingly capable and autonomous, the risks associated with this level of access will grow substantially," it added.
Here is the full text of Apple's announcement:
We give developers powerful APIs to build incredible capabilities into their apps for Apple products, backed by a set of controls designed to protect users' private data. Full Disk Access largely sidesteps these controls in order to allow backup apps to function properly on the Mac. Some developers are using Full Disk Access in ways that could put users at risk, exposing everything on their systemsβincluding files, mail, messages, and even browsing historyβwithout users' full knowledge and understanding. For communication apps, this can also compromise the privacy of the people users are communicating with.Apple did not say when the new "Full Disk Access" controls will be implemented.
Going forward, we will introduce additional controls to ensure that users who genuinely wish to grant an app this extraordinary level of access can only do so with very explicit user action. Addressing this is critical. As AI agents become increasingly capable and autonomous, the risks associated with this level of access will grow substantially. We are committed to ensuring users clearly understand these risks before granting such access, so they can make informed decisions about their own data and privacy.
This article, "Apple Announces 'Full Disk Access' Changes on macOS Due to AI Agents" first appeared on MacRumors.com
Discuss this article in our forums
AT&T and Apple Just Took Steps Toward Fixing iPhone 18 Pro Max Issue
Thousands of iPhone 18 Pro Max users have been unable to connect to the AT&T network for phone calls, text messages, and data. The underlying cause has yet to be disclosed, but AT&T and Apple continue to work toward a resolution.
First, Apple today released an AT&T carrier settings update (version 72.1, up from 72.0.2) for iPhones. Given the timing of this update, it presumably includes some optimizations related to iPhone 18 Pro Max cellular connectivity, but it is unclear if AT&T has fully resolved the issues or if further tweaks will be necessary.
When a carrier settings update is available on your iPhone, you will be prompted to install it. If you dismiss the notification or do not see it, open the Settings app, tap General β About, and the update should appear there. You can see your iPhone's current carrier settings version in the same About menu, next to your carrier's name.
Second, Apple today started requiring iPhone 18 Pro Max users to install iOS 27.0.1 in order to set up the device, even though most if not all affected users have said that the update does not resolve the issues. A leaked AT&T email said iOS 27.0.1 is meant to help mitigate future impacts, so it may still help in some way.
In a statement shared with MacRumors today, AT&T said it is "working with Apple" on this.
Apple did not respond to our request for comment.
It is not yet clear if a follow-up update like iOS 27.0.2 will be required as part of a fix, or if the carrier update alone will be sufficient.
This article, "AT&T and Apple Just Took Steps Toward Fixing iPhone 18 Pro Max Issue" first appeared on MacRumors.com
Discuss this article in our forums
Distribution Release: Mainstream OS 3.0.0
Announcing new builds for 2 October 2026
- Beta: Build 26220.9587
- Experimental: Build 26340.9596
- Beta (26H1): Build 28020.3142
- Experimental (26H1): Build 28120.3151
- Experimental (Future Platforms): Build 29680.1000
AT&T on iPhone 18 Pro Max Issues: 'We Are Working With Apple'
"Apple has released a software update to address an issue that may affect calls, data, texts, and 911 calls on a small number of Apple iPhone 18 Pro Max phones," an AT&T spokesperson said. "AT&T customers with an iPhone 18 Pro Max should update their phones now to the latest available software. Our top priority is ensuring customers can stay connected when they need it most, and we are working with Apple to make it right."
As we reported last week, affected iPhone 18 Pro Max users are unable to connect to the AT&T network for phone calls, text messages, or data either persistently or intermittently, and their devices typically enter SOS mode as a result. The underlying cause has not been confirmed, and Apple has yet to publicly comment on the matter.
Thousands of users have experienced this problem since the iPhone 18 Pro Max launched on September 18, according to discussion threads across the Apple Support Community, Reddit, MacRumors Forums, and other online platforms.
Notably, the U.S. version of the iPhone 18 Pro Max is the only new iPhone unveiled last month that is equipped with a Qualcomm cellular modem. The smaller iPhone 18 Pro and the iPhone Duo are equipped with Apple's C2 modem worldwide, as is the iPhone 18 Pro Max in all other countries. However, it is unclear if this is a contributing factor.
AT&T said iPhone 18 Pro Max users who have experienced or continue to experience issues can return to an Apple or AT&T store for additional support.
Yesterday, AT&T sent a text message to some customers advising them to update to iOS 27.0.1 immediately, even though that update does not appear to resolve the issues, according to comments shared by affected users.
In an alleged internal email shared on Reddit today, AT&T clarified that its guidance about updating to iOS 27.0.1 is meant to help mitigate future impacts, but it is unclear if or how the update actually helps. Apple's release notes for iOS 27.0.1 only mentioned fixes for other bugs, including a Face ID issue affecting iPhone 18 Pro models.
The alleged email added that customers can visit an Apple or AT&T store for a replacement device, but some customers said their request for a replacement under warranty was declined. With this issue becoming more widespread, hopefully Apple and AT&T are working to inform employees about the issue and implement a formal process.
iPhone 18 Pro Max users should be aware that today is the final day to return the device to Apple under the company's standard 14-day return policy in the U.S., for customers who purchased or received it on September 18 launch day.
Typically, Apple is able to address issues like these with software updates, so hopefully it will be able to fix the issue in iOS 27.0.2 or later. Hardware issues are more rare, but when they do happen, Apple sometimes launches a free repair program. We will continue to provide further updates on this topic as more information becomes available.
This article, "AT&T on iPhone 18 Pro Max Issues: 'We Are Working With Apple'" first appeared on MacRumors.com
Discuss this article in our forums
Beats 360 Cushion Kits and Apple Accessories See Early Prime Day Price Drops
Early Prime Day Deals
- What's the deal? Shop early Prime Day deals on Apple-related accessories and more
- Where can I get it? Amazon
- Where can I find the original deal? Right here
Amazon's fall Prime Day, called Prime Big Deal Days, is nearly upon us, and early discounts have already started appearing online. We're tracking big markdowns on TVs, monitors, charging accessories, and more in our original article, and be sure to keep an eye on the MacRumors front page for more Prime Big Deal Days news as the event gets closer.
Beats 360
- What's the deal? Save on Beats 360 cushion kits and more
- Where can I get it? Amazon
- Where can I find the original deal? Right here
The new Beats 360 headphones were announced last week, and they're now available to order on Amazon and other retailers. They're available in Black, Cloud, plus a bundle where you can style the headphones however you'd like thanks to interchangeable ear cushions and headbands.
Alongside the launch, Beats announced that its Cushion Kits compatible with the Beats 360 are available for just $9.99 for the next month, down from $49.99. Stock on these has begun to dwindle since launch, so be sure to get in soon before they disappear.
Mac Mini and Mac Studio
- What's the deal? Take up to $50 off Mac mini and Mac Studio
- Where can I get it? Amazon
- Where can I find the original deal? Right here
Amazon recently introduced the first cash discounts on Apple's brand new Mac mini and Mac Studio, with discounts reaching up to $50 off original prices. All of these deals represent the first discounts on the new computers, which just launched last month.
Apple Watch Ultra 4
- What's the deal? Take $19 off Apple Watch Ultra 4
- Where can I get it? Amazon
- Where can I find the original deal? Right here
The new Apple Watch Ultra 4 just launched last month, and Amazon is offering the first cash discounts on select models for the first time since their launch. You can get the Apple Watch Ultra 4 for $779.99, down from $799.00.
If you're on the hunt for more discounts, be sure to visit our Apple Deals roundup where we recap the best Apple-related bargains of the past week.
Deals Newsletter
Interested in hearing more about the best deals you can find in 2026? Sign up for our Deals Newsletter and we'll keep you updated so you don't miss the biggest deals of the season!
This article, "Beats 360 Cushion Kits and Apple Accessories See Early Prime Day Price Drops" first appeared on MacRumors.com
Discuss this article in our forums
Upgrading From an Older Apple Watch? Here's How the New Features Stack Up
The Apple Watch Series 12 brings a more substantial set of changes than the Series 11, which was one of the smallest refreshes in the device's history. While Apple has discontinued previous Series models, older watches may still be available at lower prices from third-party retailers or secondhand.
Read on to see the breakdown below for each new feature, change, and improvement that was added with each Apple Watch model compared to its direct predecessor. Viewing each generation's changes in this way helps weigh up the accumulative upgrades one receives by skipping multiple generations when buying a new device, as well as get a sense of which generations offered the biggest overall upgrades.
Apple Watch Series 12 (2026)
- S11 chip (based on A20 Pro)
- 4GB of RAM
- New Health Sensing System
- Second-generation electrical heart sensor
- Fourth-generation, always-on optical heart sensor
- Background heart rate measurements every five seconds throughout the day
- Heart rate variability measured up to 24x more often
- Recovery HRV metric
- Readiness app
- Daytime view in the Vitals app
- More accurate step tracking
- Steps and real-time heart rate watch face complications
- Audio Intelligence with Sound Recognition and automatic Shazam detection
- Live Rewind and Siri Recap
- Ceramic Shield 2 front glass, 60% tougher than Series 11's Ion-X glass (aluminum models only)
- Aluminum or titanium and zirconia back replaces metal and sapphire crystal back
- Up to 10 hours of outdoor workout tracking (25% longer than Series 11)
- 15 minutes of charging provides up to 12 hours of normal use (up from eight hours)
- Five minutes of charging provides up to 10 hours of sleep tracking (up from eight hours)
- Aluminum and titanium cases are 1mm wider and around two grams heavier
- Slightly larger bezels and display area is approximately 2% smaller despite unchanged screen resolutions
- Ceramic case option introduced in Pearl White and Night Blue, with slightly larger dimensions than aluminum and titanium models
- Dark Bronze, Light Gold, and Black aluminum finishes replace Silver, Rose Gold, and Jet Black; Space Gray remains available
- Radiant Gold titanium replaces Gold and Slate; Natural titanium remains available
Apple Watch Series 11 (2025)
- 2x more scratch-resistant Ion-X glass (aluminum models only)
- 5G connectivity (cellular models only)
- 7β10% larger battery
- Space Gray aluminum option reintroduced
Apple Watch Series 10 (2024)
- Refined design with a thinner casing, display with more rounded edges and a wider aspect ratio, and front glass that extends further over the sides for a higher screen-to-body ratio
- 42mm and 46mm case sizes
- Larger displays
- LTPO3 display for faster refresh rate in always-on mode, enabling a live ticking seconds hand on select watch faces
- Wide-angle OLED (up to 40% brighter when viewed at an angle)
- S10 chip (same CPU as S9, but more compact)
- Water temperature sensor
- Depth app and Depth gauge to 6m
- Microphone with voice isolation
- Redesigned speaker with media playback
- Metal back with larger charging coil and integrated antenna
- Faster charging (0β80% in about 30 minutes)
- Titanium casing replaces stainless steel option, now in Silver (matches previous stainless steel casing, Apple Watch Hermès only), Gold, Slate, and Natural
- Starlight, Midnight, Pink, and (PRODUCT)RED finishes discontinued, replaced with Silver, Rose Gold, and polished Jet Black finishes
Apple Watch Series 9 (2023)
- Retina display with up to 2,000 nits of brightness
- Display can get dimmer in dark rooms and at night (down to 1 nit of brightness)
- Double tap gesture
- Wrist flick gesture
- Sleep apnea detection
- Hypertension detection
- S9 chip (based on A15 Bionic)
- 2GB of RAM
- 4-core Neural Engine (2x faster than Series 8)
- βSiriββ processed on-device for requests that do not need information from the internet
- ββSiriββ can access health and fitness data
- 25% more accurate dictation
- Second-generation Ultra Wideband chip
- Precision Finding for iPhone 15 models
- HomePod proximity integration
- 64GB storage
- Pink aluminum casing introduced
Apple Watch Series 8 (2022)
- S8 chip (same CPU as S6)
- More powerful gyroscope and high dynamic range accelerometer
- βCrash Detection
- Body temperature sensor
- Wrist temperature sensing during sleep mem
- Cycle tracking with retrospective ovulation estimates
- Blue and Green aluminum casings discontinued, Silver aluminum reintroduced
- Apple Watch Edition discontinued
Apple Watch Series 7 (2021)
- 41mm and 45mm case sizes
- 1.7mm display borders for almost 20% more screen area than Series 6
- QWERTY keyboard
- Up to 70% brighter display than Series 6 indoors when wrist is down
- S7 chip (Same CPU as S6)
- Crack-resistant front crystal (50% thicker)
- IP6X dust resistance
- Fast charging for up to 80% charge in about 45 minutes (33% faster than Series 6)
- Silver and Space Gray aluminum casings discontinued, Midnight, Starlight, and Green introduced
Apple Watch Series 6 (2020)
- Up to 2.5x brighter always-on display
- Access Notification Center, Control Center, and Complications when display is asleep
- S6 chip (based on A13 Bionic, 20% faster than S5)
- 1.5GB of RAM
- Always-on altimeter
- First-generation Ultra Wideband chip
- 2.4GHz and 5GHz Wi-Fi
- Blood oxygen sensor
- Faster charging for a full charge in under 1.5 hours
- Improved battery life for tracking certain workouts, such as indoor and outdoor runs
- Blue and (PRODUCT)RED aluminum casing options introduced
- Space Black stainless steel casing available with Apple Watch Hermès only, Graphite option introduced
- Ceramic Apple Watch Edition discontinued
Apple Watch Series 5 (2019)
- Always-on Retina display
- Force Touch removed
- S5 chip
- Compass
- 32GB storage
- Apple Watch Edition reintroduced with White Ceramic, Natural Titanium, and Space Black Titanium casing options
Apple Watch Series 4 (2018)
- 40mm and 44mm case sizes
- Over 30% larger display with curved corners
- 3.0mm display borders
- S4 chip (2x faster than S3)
- Gyroscope and accelerometer
- Barometric altimeter
- Fall detection
- 2.4GHz Wi-Fi
- Optical heart sensor and electrical heart sensor for ECG
- 16GB storage
- Digital Crown with haptic feedback
- 50% louder speaker
- Relocated microphone for reduced echo and better sound quality
- Faster charging for a full charge in 1.5 hours
- Gold stainless steel casing introduced
- Apple Watch Edition discontinued
Apple Watch Series 3 (2017)
- S3 chip (70% faster than S2)
- Barometric altimeter
- Bluetooth 4.2
- βSiriβ able to speak through the onboard speaker
- LTE cellular model available with red Digital Crown
- Rose Gold aluminum casing discontinued
- Gray Ceramic Apple Watch Edition model introduced
Apple Watch Series 2 (2016)
- Retina display with up to 1,000 nits of brightness (2x brighter than previous generation)
- S2 chip (dualβcore)
- 1GB of RAM
- GPS and GLONASS receiver
- Water resistant up to 50 meters
- Gold and Rose Gold aluminum casing options added
- 18K Gold and Rose Gold Apple Watch Edition discontinued, Ceramic model introduced in White
- Apple Watch Nike+ variant introduced
The Series 12 has a stronger case than the Series 11 as an upgrade in general, but most Series 11 owners still have little reason to get a new device. Its new heart sensors, more frequent HRV measurements, Readiness app, and longer workout battery life matter most if you rely on your Apple Watch for training and recovery insights. Sound Recognition may also appeal to deaf or hard of hearing users, while the ceramic case offers a finish unavailable with the Series 11.
Upgrading from a Series 10 brings these changes alongside the Series 11's 5G connectivity on cellular models and longer rated everyday battery life. From a Series 9, you would also gain the thinner Series 10 design, larger display, depth gauge, and water temperature sensor.
The cumulative upgrades become more compelling for Series 8 users and older, particularly if battery wear is affecting daily use. Series 7 owners gain Crash Detection and temperature sensing as well as the newer design and Series 12 health features. The biggest differences will be felt by those with a Series 6 or older, though the condition of your current watch and the cost of replacing it still matter.
watchOS 27 supports Series 9 and later, leaving the Series 8 and older without the latest major software release. That does not mean they will immediately stop working or receiving all security updates, but they are now considered old devices. If you are shopping for a replacement, a discounted or secondhand Series 11 may offer better value if you do not need the Series 12's health features, providing you can find it at a sufficiently low price. watchOS 26 is not supported by the Series 5 or older, so anyone with one of these devices should definitely consider upgrading soon.
Looking to the future, it looks like next year's Apple Watch Series 13 could offer some major upgrades including a redesign and new sensors. This could make it a great upgrade for those with a Series 11 or older.
This article, "Upgrading From an Older Apple Watch? Here's How the New Features Stack Up" first appeared on MacRumors.com
Discuss this article in our forums
Ben Hutchings: FOSS activity in September 2026
- Debian packages:
- firmware-nonfree:
- Bugs:
- Merge requests:
- opened and merged !154: Update to 20260916; fix some license information
- Uploads:
- uploaded version 20260810-1~bpo13+1 to trixie-backports
- uploaded version 20260916-1 to unstable
- initramfs-tools:
- Merge requests:
- kernel-team:
- Merge requests:
- linux:
- Bugs:
- Merge requests:
- closed !2010: [x86] Backport security fixes for KVM (LTS)
- merged !2083: linux-image: Bump Pre-Depends version on linux-base (>= 4.17~)
- reviewed and merged !2095: 6.1 stable updates (LTS)
- merged !2108: [alpha] Reenable CONFIG_COMPACTION
- merged !2109: Enable BMI323 support as a module
- merged !2110: riscv64 config update for linux 7.3
- Uploads:
- uploaded version 7.1.13-1~bpo13+1 to trixie-backports
- uploaded version 7.2.6-1~bpo13+1 to trixie-backports
- linux-6.1 (LTS):
- linux-6.12 (LTS):
- Uploads:
- uploaded version 6.12.107-1~deb12u1 to bookworm-security
- merged changes from trixieβs 6.12.111-1
- Uploads:
- linux-base:
- Uploads:
- uploaded version 4.17~bpo13+1 to trixie-backports
- Uploads:
- nfs-utils:
- Merge requests:
- wireless-regdb:
- Uploads:
- uploaded version 2026.09.03-1 to unstable
- Uploads:
- firmware-nonfree:
- Mailing lists:
Apple Store in UK Reopening on iPhone Duo Launch Day
Last year, Apple permanently closed its store in the heart of nearby Bristol, but Apple Cribbs Causeway remains open inside a shopping center on the outskirts of the city. In other UK store news, Apple Lakeside in Essex was also renovated and reopened in September.
This article, "Apple Store in UK Reopening on iPhone Duo Launch Day" first appeared on MacRumors.com
Discuss this article in our forums
Distribution Release: Koozali SME Server 11.0
-
Planet Debian
- Junichi Uekawa: Tokyo has been raining for many days but finally there's some sunny days.
Junichi Uekawa: Tokyo has been raining for many days but finally there's some sunny days.
AT&T Acknowledges iPhone 18 Pro Max Issues: Update 'Immediately'
AT&T said that customers should immediately update to iOS 27.0.1, despite the fact that many affected users have said that the update does not resolve the issues. There are now at least thousands of users that have experienced this problem, according to discussion topics across the Apple Support Community, Reddit, and MacRumors Forums.
Here is what the text message says:
Hi, it's AT&T. Action needed: An important Apple software update for your iPhone 18 Pro Max is available. Some phones may be unable to complete calls/text/data, including to 911. Update to iOS 27.0.1 immediately β Settings > General > Software Update. Please contact AT&T or Apple if you have questions.Apple's release notes for iOS 27.0.1 did not mention a fix for AT&T cellular issues. Rather, the update fixed a Face ID issue on the iPhone 18 Pro models.
As we reported last week, affected iPhone 18 Pro Max users are unable to connect to the AT&T network for phone calls, text messages, or data either persistently or intermittently, and their devices typically enter SOS mode as a result. The underlying cause has not been confirmed, and Apple has yet to publicly comment on the matter.
Notably, the U.S. version of the iPhone 18 Pro Max is the only new iPhone unveiled last month that is equipped with a Qualcomm cellular modem. The smaller iPhone 18 Pro and the iPhone Duo are equipped with Apple's C2 modem worldwide, as is the iPhone 18 Pro Max in all other countries. However, it is unclear if this is a contributing factor.
If a software issue is identified, then another update such as iOS 27.0.2 may be required. If there is a hardware issue, though, then this would obviously be a much bigger problem. We will provide further updates as new information becomes available.
This article, "AT&T Acknowledges iPhone 18 Pro Max Issues: Update 'Immediately'" first appeared on MacRumors.com
Discuss this article in our forums
Distribution Release: umbrelOS 2.0.0
Development Release: Ubuntu 26.10 Beta
New 'AirFly Drive' Accessory Turns Wired CarPlay Into Wireless CarPlay
AirFly Drive uses Wi-Fi to establish a connection between your iPhone and vehicle, eliminating the need to use a USB-C cable for CarPlay.
Priced at $39.99 in the U.S., the AirFly Drive plugs into a USB data port in your vehicle. The accessory is essentially a compact aluminum cube with a USB-A port on it, with a USB-C adapter included in the box. It is designed to stay plugged in, so that you can get in your vehicle and use wireless CarPlay right away every time.
You only need to pair your iPhone with the AirFly Drive once, and then it will offer fast automatic reconnection every time the vehicle starts, with no app, account, or charging required. A button on the accessory allows for one-press switching between two paired iPhones, which is useful if two people share the same vehicle.
Before first-time setup, you must disconnect your iPhone from your vehicle by choosing "Forget This Device" in the vehicle's Bluetooth settings.
Twelve South says the AirFly Drive works with any iPhone running iOS 10 or newer and any vehicle that already has factory support for wired CarPlay.
Similar wired-to-wireless CarPlay dongles from other brands are available on Amazon, but Twelve South is a reputable maker of accessories for Apple products. AirFly Drive is available to order now in white or black.
This article, "New 'AirFly Drive' Accessory Turns Wired CarPlay Into Wireless CarPlay" first appeared on MacRumors.com
Discuss this article in our forums
Apple Shares Photos Shot on iPhone Duo
iPhone Duo is equipped with four cameras:
- A rear 48-megapixel Fusion Main camera
- A rear 48-megapixel Ultra Wide camera
- A front 12-megapixel Center Stage camera in the outer display
- A front camera under the inner display
You can pre-order the iPhone Duo starting Friday, October 16 at 5 a.m. Pacific Time.
This article, "Apple Shares Photos Shot on iPhone Duo" first appeared on MacRumors.com
Discuss this article in our forums
Apple Says iPhone Duo Has Replaceable 'Cover Layer'
"It's actually replaceable, which is a really nice thing for the customer that you can peel off that layer and get a new one," said Bergeron.
In the U.S., customers who purchase AppleCare+ coverage for the iPhone Duo can get the cover layer replaced for $19, according to Apple's website. The cost to replace the cover layer without coverage has yet to be disclosed.
The cover layer helps to hide the crease in the middle of the foldable inner display. Bergeron said Apple did a "significant amount of engineering work" to ensure that this layer remains in good condition for as long as possible, but customers will have the option to replace it if it wears down or the crease becomes more visible over time.
Officially, the ability to replace the iPhone Duo's cover layer will likely be limited to Apple Stores and Apple Authorized Service Providers. It remains to be seen if customers will be able to easily remove the layer themselves.
iPhone Duo pre-orders begin on Friday, October 16 at 5 a.m. Pacific Time, and the device will launch in stores on Friday, October 23.
This article, "Apple Says iPhone Duo Has Replaceable 'Cover Layer'" first appeared on MacRumors.com
Discuss this article in our forums
Distribution Release: Nitrux 7.0.0
Apple's Smart Home Camera Will Apparently Have 'No Video Recording'
Instead, he described the rumored product as a sensor that will be capable of "analyzing your environment in your home" using AI. "This is going to be the first mainstream video camera that's a home video security camera that doesn't give you any footage," he said.
This article, "Apple's Smart Home Camera Will Apparently Have 'No Video Recording'" first appeared on MacRumors.com
Discuss this article in our forums