Supported way for an arm64 process to map below the 4 GB __PAGEZERO floor?

Hi Quinn — following up from DTS case 22070584.

I'm working on a Windows compatibility runtime (Wine plus a CPU translator) that runs natively on Apple Silicon. 64-bit x86 Windows programs work fine. 32-bit ones don't, because they need address space in the low 4 GB: guest pointers are 32-bit, and some Windows structures sit at fixed addresses like 0x7ffe0000 that programs read directly.

On arm64 I can't get anything down there:

task_info(TASK_VM_INFO) -> min_address 0x100ea0000 mmap(0x7ffe0000, MAP_FIXED) -> ENOMEM mach_vm_allocate(0x7ffe0000, VM_FLAGS_FIXED) -> KERN_INVALID_ADDRESS

There's also nothing below 4 GB to remove: mach_vm_region finds no entry there at all, and mach_vm_deallocate(0, 4 GB) returns KERN_SUCCESS without changing anything. Building with a smaller __PAGEZERO doesn't help either — every size I tried (0x1000, 0x4000, 0x10000, 0x100000, 0x1000000, 0x10000000, 0x80000000) gets SIGKILLed before main, with no crash report. Ad-hoc signing, the hardened runtime and -no_pie made no difference.

I did notice /usr/libexec/rosetta/runtime is arm64 with no __PAGEZERO segment at all and __TEXT at vmaddr 0, so the kernel can clearly do this, at least for platform binaries.

Is there a supported way for a third-party arm64 process to map below 4 GB — an entitlement, a spawn attribute, something I've missed? If the answer is no, that's fine, I'd just like to know so I can stop looking and plan around it.

I have two small test programs that print all of the above if they'd be useful.

Answered by DTS Engineer in 906401022

I sat down to play with this today and I want to share some results.

Using Xcode 27.0 on macOS 26.6.1, I created a new app target and then gutted all the app bits, à la Signing a daemon with a restricted entitlement.

I then added a main.swift with this code:

import Darwin

func main() {
    print("Hello Cruel World!")
    var addr: vm_address_t = 0x7ffe0000
    // Annoyingly, using `vm_page_size` makes Swift concurrency unhappy, so
    // let’s get this from BSD.
    let pageSize = vm_size_t(bitPattern: Int(getpagesize())) 
    print("page size: 0x\(String(pageSize, radix: 16))")
    let kr = vm_allocate(mach_task_self_, &addr, pageSize, VM_FLAGS_FIXED)
    if kr == KERN_SUCCESS {
        print("did allocate, addr: 0x\(String(addr, radix: 16))")
    } else {
        print("did not allocate, kr: \(kr)")
    }
    if false {
        print("will pause, pid: \(getpid())")
        pause()
        print("did pause")
    }
}

main()

On running that, I saw this:

Hello Cruel World!
page size: 0x4000
did not allocate, kr: 1

where 1 is KERN_INVALID_ADDRESS. So far, so expected.

To get this to work I had to change two things. First, in Signing & Capabilities I added the Cross-architecture Compatibility Framework capability [1]. This added the com.apple.developer.cross-architecture-support entitlement. Xcode’s automatic code signing kicked in and generated a provisioning profile that authorises my app to claim that entitlement.

When I ran that version the allocation failed with error 3, or KERN_NO_SPACE. Which brings me to the second change: adding an explicit zero page segment. Specifically, I edited the Other Linker Flags build setting to look like this:

-Xlinker
-pagezero_size
-Xlinker
0x4000
$(inherited)

And with change, the allocation works:

Hello Cruel World!
page size: 0x4000
did allocate, addr: 0x7ffe0000

Yay!

I think that’s all you need, but lemme know if you hit any snags.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] No docs for this yet, alas (r. 188189100).

I don't have an answer, however the latest CrossOver beta can use FEX to run 32-bit Windows programs too. Maybe check what it's doing.

Accepted Answer

Thanks for bringing this to the forums.

I need to do research this before I can give you a definitive answer. I’ll reply here when I know more.

ps For those following along at home, note that this came up in other thread but we never drove it to a conclusion there, so I’m gonna use this thread to do that.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I’m making good progress on this. I need to do some due diligence at my end before I say anything definitive, but there is one bit of info that’s clearly public and thus I can pass along…

If you look in the Signing & Capabilities editor in Xcode 27.0 you’ll see that it has a new Cross-architecture Compatibility Framework capability. If you add that to your app, Xcode updates your project to add a claim for the com.apple.developer.cross-architecture-support entitlement and approval to use that entitlement in your provisioning profile.

AFAICT we don’t have any official docs about that yet, hence the caution I opened this response with, but if you search the ’net you’ll find other folks talking about the entitlement.

I hope to have another update here early next week.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

WIll wait, but this was helpful too.

I sat down to play with this today and I want to share some results.

Using Xcode 27.0 on macOS 26.6.1, I created a new app target and then gutted all the app bits, à la Signing a daemon with a restricted entitlement.

I then added a main.swift with this code:

import Darwin

func main() {
    print("Hello Cruel World!")
    var addr: vm_address_t = 0x7ffe0000
    // Annoyingly, using `vm_page_size` makes Swift concurrency unhappy, so
    // let’s get this from BSD.
    let pageSize = vm_size_t(bitPattern: Int(getpagesize())) 
    print("page size: 0x\(String(pageSize, radix: 16))")
    let kr = vm_allocate(mach_task_self_, &addr, pageSize, VM_FLAGS_FIXED)
    if kr == KERN_SUCCESS {
        print("did allocate, addr: 0x\(String(addr, radix: 16))")
    } else {
        print("did not allocate, kr: \(kr)")
    }
    if false {
        print("will pause, pid: \(getpid())")
        pause()
        print("did pause")
    }
}

main()

On running that, I saw this:

Hello Cruel World!
page size: 0x4000
did not allocate, kr: 1

where 1 is KERN_INVALID_ADDRESS. So far, so expected.

To get this to work I had to change two things. First, in Signing & Capabilities I added the Cross-architecture Compatibility Framework capability [1]. This added the com.apple.developer.cross-architecture-support entitlement. Xcode’s automatic code signing kicked in and generated a provisioning profile that authorises my app to claim that entitlement.

When I ran that version the allocation failed with error 3, or KERN_NO_SPACE. Which brings me to the second change: adding an explicit zero page segment. Specifically, I edited the Other Linker Flags build setting to look like this:

-Xlinker
-pagezero_size
-Xlinker
0x4000
$(inherited)

And with change, the allocation works:

Hello Cruel World!
page size: 0x4000
did allocate, addr: 0x7ffe0000

Yay!

I think that’s all you need, but lemme know if you hit any snags.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] No docs for this yet, alas (r. 188189100).

Thanks Quinn, that worked for me too.

One thing I noticed while playing with it: the small page-zero isn't actually needed. With the entitlement, the space below the image becomes a reservation I can free myself. So I kept the default 4 GB page-zero, called vm_deallocate on it at startup, and could then map 0x10000, 0x7ffe0000 and 0xfffe0000 every time. With just the 16 KB page-zero, 0x7ffe0000 works but 0x10000 never does, because the slide gap is still reserved.

On packaging: a nested .app in Contents/Helpers that reuses the same bundle ID and profile gets the entitlement fine, and it notarized without issues. A bare helper binary gets killed at launch.

Two quick questions:

Is reusing the parent's bundle ID and profile for a nested helper app okay to rely on, or should each helper get its own App ID?

Is there any supported way to give the entitlement to a plain helper executable? Our loader isn't a bundle's main executable.

Thanks again, this helps a lot.

Supported way for an arm64 process to map below the 4 GB __PAGEZERO floor?
 
 
Q