From 9debadd7e0afe4ce9d9bbc4053932e23ed80f884 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Tue, 29 Sep 2026 23:27:52 +0200 Subject: [PATCH] fix: recover redacted mac addresses on macos 27 --- pkg/util/machine/mac_address_darwin.go | 127 +++++++++++ pkg/util/machine/mac_address_darwin_test.go | 237 ++++++++++++++++++++ pkg/util/machine/mac_address_default.go | 9 + pkg/util/machine/machine.go | 2 +- pkg/util/machine/machine_test.go | 4 + 5 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 pkg/util/machine/mac_address_darwin.go create mode 100644 pkg/util/machine/mac_address_darwin_test.go create mode 100644 pkg/util/machine/mac_address_default.go diff --git a/pkg/util/machine/mac_address_darwin.go b/pkg/util/machine/mac_address_darwin.go new file mode 100644 index 00000000..e802addd --- /dev/null +++ b/pkg/util/machine/mac_address_darwin.go @@ -0,0 +1,127 @@ +//go:build darwin && !ios + +package machine + +import ( + "bytes" + "errors" + "fmt" + "net" + "unsafe" + + "github.com/ebitengine/purego" +) + +func interfaceMacAddress(networkInterface net.Interface) (string, error) { + return macAddressWithHardwareLookup(networkInterface, hardwareMacAddress) +} + +func macAddressWithHardwareLookup(networkInterface net.Interface, lookup func(string) (net.HardwareAddr, error)) (string, error) { + // macOS 27 can redact network interface addresses with a shared placeholder. + // Resolve the same interface through IOKit to preserve its existing identity. + if !bytes.Equal(networkInterface.HardwareAddr, []byte{0x02, 0, 0, 0, 0, 0}) { + return networkInterface.HardwareAddr.String(), nil + } + + address, err := lookup(networkInterface.Name) + if err != nil { + return "", fmt.Errorf("macOS redacted the mac address for %q; failed to read its hardware address: %w", networkInterface.Name, err) + } + + if len(address) != 6 || address[0]&1 != 0 || + bytes.Equal(address, []byte{0, 0, 0, 0, 0, 0}) || + bytes.Equal(address, []byte{0x02, 0, 0, 0, 0, 0}) { + return "", fmt.Errorf("macOS redacted the mac address for %q; IOKit returned no usable hardware address", networkInterface.Name) + } + + return address.String(), nil +} + +type macAddressAPI struct { + matching func(uint32, uint32, string) uintptr + service func(uint32, uintptr) uint32 + searchProperty func(uint32, string, uintptr, uintptr, uint32) uintptr + releaseObject func(uint32) int32 + createString func(uintptr, string, uint32) uintptr + release func(uintptr) + typeID func(uintptr) uintptr + dataTypeID func() uintptr + dataLength func(uintptr) int64 + dataBytes func(uintptr) unsafe.Pointer +} + +func hardwareMacAddress(name string) (net.HardwareAddr, error) { + iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_LOCAL) + if err != nil { + return nil, fmt.Errorf("load IOKit: %w", err) + } + + defer func() { _ = purego.Dlclose(iokit) }() + + coreFoundation, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_LOCAL) + if err != nil { + return nil, fmt.Errorf("load CoreFoundation: %w", err) + } + + defer func() { _ = purego.Dlclose(coreFoundation) }() + + var api macAddressAPI + + purego.RegisterLibFunc(&api.matching, iokit, "IOBSDNameMatching") + purego.RegisterLibFunc(&api.service, iokit, "IOServiceGetMatchingService") + purego.RegisterLibFunc(&api.searchProperty, iokit, "IORegistryEntrySearchCFProperty") + purego.RegisterLibFunc(&api.releaseObject, iokit, "IOObjectRelease") + purego.RegisterLibFunc(&api.createString, coreFoundation, "CFStringCreateWithCString") + purego.RegisterLibFunc(&api.release, coreFoundation, "CFRelease") + purego.RegisterLibFunc(&api.typeID, coreFoundation, "CFGetTypeID") + purego.RegisterLibFunc(&api.dataTypeID, coreFoundation, "CFDataGetTypeID") + purego.RegisterLibFunc(&api.dataLength, coreFoundation, "CFDataGetLength") + purego.RegisterLibFunc(&api.dataBytes, coreFoundation, "CFDataGetBytePtr") + + return readHardwareMacAddress(name, api) +} + +func readHardwareMacAddress(name string, api macAddressAPI) (net.HardwareAddr, error) { + matching := api.matching(0, 0, name) + if matching == 0 { + return nil, errors.New("could not create interface matching dictionary") + } + + // IOServiceGetMatchingService consumes the matching dictionary. + service := api.service(0, matching) + if service == 0 { + return nil, errors.New("network interface was not found in IOKit") + } + defer api.releaseObject(service) + + const utf8Encoding = 0x08000100 + + key := api.createString(0, "IOMACAddress", utf8Encoding) + if key == 0 { + return nil, errors.New("could not create hardware address property key") + } + defer api.release(key) + + // Apple's receipt-validation guidance reads IOMACAddress from the interface + // or its parents in the IOService plane. + // https://developer.apple.com/documentation/appstorereceipts/validating-receipts-on-the-device + const iterateRecursivelyAndParents = 0x01 | 0x02 + + property := api.searchProperty(service, "IOService", key, 0, iterateRecursivelyAndParents) + if property == 0 { + return nil, errors.New("hardware address property was not found in IOKit") + } + defer api.release(property) + + if api.typeID(property) != api.dataTypeID() || api.dataLength(property) != 6 { + return nil, errors.New("IOKit hardware address must contain six bytes") + } + + data := api.dataBytes(property) + if data == nil { + return nil, errors.New("IOKit hardware address data is missing") + } + + // Copy the bytes before releasing the CoreFoundation property. + return append(net.HardwareAddr(nil), unsafe.Slice((*byte)(data), 6)...), nil +} diff --git a/pkg/util/machine/mac_address_darwin_test.go b/pkg/util/machine/mac_address_darwin_test.go new file mode 100644 index 00000000..054f04ce --- /dev/null +++ b/pkg/util/machine/mac_address_darwin_test.go @@ -0,0 +1,237 @@ +//go:build darwin && !ios + +package machine + +import ( + "errors" + "net" + "unsafe" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("macOS hardware address", func() { + var networkInterface net.Interface + + BeforeEach(func() { + networkInterface = net.Interface{ + Name: "en4", + HardwareAddr: net.HardwareAddr{0x02, 0, 0, 0, 0, 0}, + } + }) + + It("preserves an existing unredacted identity without consulting IOKit", func() { + networkInterface.HardwareAddr = net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} + + address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) { + Fail("unexpected IOKit lookup") + + return nil, nil + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(address).To(Equal("aa:bb:cc:dd:ee:ff")) + }) + + It("recovers the hardware address of the selected interface when redacted", func() { + address, err := macAddressWithHardwareLookup(networkInterface, func(name string) (net.HardwareAddr, error) { + Expect(name).To(Equal("en4")) + + return net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, nil + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(address).To(Equal("aa:bb:cc:dd:ee:ff")) + }) + + It("reports unavailable hardware information without returning the placeholder", func() { + lookupErr := errors.New("interface unavailable") + address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) { + return nil, lookupErr + }) + + Expect(address).To(BeEmpty()) + Expect(errors.Is(err, lookupErr)).To(BeTrue()) + Expect(err.Error()).To(ContainSubstring("macOS redacted the mac address for \"en4\"")) + }) + + DescribeTable("rejects an unusable IOKit address", func(hardware net.HardwareAddr) { + address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) { + return hardware, nil + }) + + Expect(address).To(BeEmpty()) + Expect(err).To(MatchError(ContainSubstring("IOKit returned no usable hardware address"))) + }, + Entry("missing", nil), + Entry("wrong length", net.HardwareAddr{0xaa, 0xbb}), + Entry("zero", net.HardwareAddr{0, 0, 0, 0, 0, 0}), + Entry("redacted", net.HardwareAddr{0x02, 0, 0, 0, 0, 0}), + Entry("broadcast", net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}), + Entry("multicast", net.HardwareAddr{0x01, 0, 0x5e, 0, 0, 1}), + ) +}) + +var _ = Describe("IOKit hardware address lookup", func() { + const ( + dictionary = uintptr(1) + service = uint32(2) + key = uintptr(3) + property = uintptr(4) + dataType = uintptr(5) + ) + + var ( + api macAddressAPI + data []byte + released []uintptr + releasedObjects []uint32 + ) + + BeforeEach(func() { + data = []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} + released = nil + releasedObjects = nil + api = macAddressAPI{ + matching: func(port, options uint32, name string) uintptr { + Expect(port).To(BeZero()) + Expect(options).To(BeZero()) + Expect(name).To(Equal("en4")) + + return dictionary + }, + service: func(port uint32, matching uintptr) uint32 { + Expect(port).To(BeZero()) + Expect(matching).To(Equal(dictionary)) + + return service + }, + searchProperty: func(object uint32, plane string, propertyKey, allocator uintptr, options uint32) uintptr { + Expect(object).To(Equal(service)) + Expect(plane).To(Equal("IOService")) + Expect(propertyKey).To(Equal(key)) + Expect(allocator).To(BeZero()) + Expect(options).To(Equal(uint32(3))) + + return property + }, + releaseObject: func(object uint32) int32 { + releasedObjects = append(releasedObjects, object) + + return 0 + }, + createString: func(allocator uintptr, value string, encoding uint32) uintptr { + Expect(allocator).To(BeZero()) + Expect(value).To(Equal("IOMACAddress")) + Expect(encoding).To(Equal(uint32(0x08000100))) + + return key + }, + release: func(object uintptr) { + released = append(released, object) + if object == property { + clear(data) + } + }, + typeID: func(object uintptr) uintptr { + Expect(object).To(Equal(property)) + + return dataType + }, + dataTypeID: func() uintptr { return dataType }, + dataLength: func(object uintptr) int64 { + Expect(object).To(Equal(property)) + + return int64(len(data)) + }, + dataBytes: func(object uintptr) unsafe.Pointer { + Expect(object).To(Equal(property)) + + return unsafe.Pointer(&data[0]) + }, + } + }) + + It("copies the address before releasing the property, key, and interface", func() { + address, err := readHardwareMacAddress("en4", api) + + Expect(err).ToNot(HaveOccurred()) + Expect(address.String()).To(Equal("aa:bb:cc:dd:ee:ff")) + Expect(released).To(Equal([]uintptr{property, key})) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) + + It("handles a missing matching dictionary", func() { + api.matching = func(uint32, uint32, string) uintptr { return 0 } + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("could not create interface matching dictionary")) + Expect(released).To(BeEmpty()) + Expect(releasedObjects).To(BeEmpty()) + }) + + It("handles a missing interface without releasing the consumed dictionary twice", func() { + api.service = func(uint32, uintptr) uint32 { return 0 } + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("network interface was not found in IOKit")) + Expect(released).To(BeEmpty()) + Expect(releasedObjects).To(BeEmpty()) + }) + + It("releases the interface when the property key cannot be created", func() { + api.createString = func(uintptr, string, uint32) uintptr { return 0 } + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("could not create hardware address property key")) + Expect(released).To(BeEmpty()) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) + + It("releases the interface and key when the property is missing", func() { + api.searchProperty = func(uint32, string, uintptr, uintptr, uint32) uintptr { return 0 } + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("hardware address property was not found in IOKit")) + Expect(released).To(Equal([]uintptr{key})) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) + + It("rejects a non-data property without accessing its bytes", func() { + api.typeID = func(uintptr) uintptr { return dataType + 1 } + api.dataLength = nil + api.dataBytes = nil + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("IOKit hardware address must contain six bytes")) + Expect(released).To(Equal([]uintptr{property, key})) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) + + It("rejects an unexpected length without accessing the bytes", func() { + api.dataLength = func(uintptr) int64 { return 8 } + api.dataBytes = nil + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("IOKit hardware address must contain six bytes")) + Expect(released).To(Equal([]uintptr{property, key})) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) + + It("handles missing data without dereferencing a null pointer", func() { + api.dataBytes = func(uintptr) unsafe.Pointer { return nil } + + _, err := readHardwareMacAddress("en4", api) + + Expect(err).To(MatchError("IOKit hardware address data is missing")) + Expect(released).To(Equal([]uintptr{property, key})) + Expect(releasedObjects).To(Equal([]uint32{service})) + }) +}) diff --git a/pkg/util/machine/mac_address_default.go b/pkg/util/machine/mac_address_default.go new file mode 100644 index 00000000..1a5b1bf8 --- /dev/null +++ b/pkg/util/machine/mac_address_default.go @@ -0,0 +1,9 @@ +//go:build !darwin || ios + +package machine + +import "net" + +func interfaceMacAddress(networkInterface net.Interface) (string, error) { + return networkInterface.HardwareAddr.String(), nil +} diff --git a/pkg/util/machine/machine.go b/pkg/util/machine/machine.go index d2376e26..14796c46 100644 --- a/pkg/util/machine/machine.go +++ b/pkg/util/machine/machine.go @@ -44,7 +44,7 @@ func (*machine) MacAddress() (string, error) { for _, netInterface := range interfaces { addr := netInterface.HardwareAddr.String() if addr != "" { - return addr, nil + return interfaceMacAddress(netInterface) } } diff --git a/pkg/util/machine/machine_test.go b/pkg/util/machine/machine_test.go index fe8b9a6b..3d1a11d0 100644 --- a/pkg/util/machine/machine_test.go +++ b/pkg/util/machine/machine_test.go @@ -57,6 +57,10 @@ var _ = Describe("Machine", func() { res, err := machine.MacAddress() Expect(err).ToNot(HaveOccurred()) Expect(res).To(ContainSubstring(":")) + + if runtime.GOOS == "darwin" { + Expect(res).ToNot(Equal("02:00:00:00:00:00")) + } }) })