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.

Glad to hear you’re making progress. And thanks for the vm_deallocate tip.

should each helper get its own App ID?

Yes.

A few years back I helped a developer debug a very weird TestFlight issue where things failed in that environment because their app and one of their helpers had the same App ID. Finding that was ‘fun’ (s. 1997177). Those scars are what guided the bundle ID advice in Creating distribution-signed code for macOS (search for pig-jato).

Is there any supported way to give the entitlement to a plain helper executable?

Not as such (r. 125850707). The standard workaround is described in Signing a daemon with a restricted entitlement. While that’s focused on daemons, the process work for helper tools and other unbundled executables.

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

Thanks Quinn, that's really helpful. I'll give the helper its own App ID and wrap the loader in a small app bundle, following the daemon article. Appreciate you digging into this.

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