How Dopamine Implements Physical Memory Read/Write (physrw): Direct Mapping vs PTE Methods Explained
Dopamine implements physical memory read/write through two complementary mechanisms: a direct-mapping approach that maps the entire kernel physical address space into user-space, and a page-table-entry (PTE) fallback that temporarily maps individual pages on demand.
The physrw subsystem in Dopamine is the foundation for kernel introspection and patching. Located in BaseBin/libjailbreak/src/, this code allows the jailbreak to safely access physical memory from a sandboxed user process—critical for manipulating kernel data structures without triggering security mitigations. Whether you're analyzing the source for security research or building compatible tools, understanding these implementations reveals how modern iOS jailbreaks bypass hardware memory protections.
Direct Mapping: The Primary physrw Implementation
The direct-mapping method (physrw.c) creates a large, persistent window over the kernel's physical address space. This is the preferred approach when the hardware and iOS version permit it.
How the Mapping Works
In physrw.c, the physrw_handoff() function establishes the window by:
- Retrieving the current process's
pmapstructure - Calling
pmap_map_in()to map the kernel physical range into the user process - Computing
PPLRW_USER_MAPPING_OFFSETasL1_BLOCK_SIZE * L1_BLOCK_COUNT - 0x3000000000
This offset calculation (lines 87‑93) ensures the mapping starts beyond first-level translation limits, fixing address-space-overflow issues on newer iOS devices.
Physical-to-User Address Translation
The helper physrw_phystouaddr() performs the critical translation:
// Simplified logic from physrw.c
void *physrw_phystouaddr(uint64_t pa) {
// Validate against known physical range
if (pa < physBase || pa >= physBase + physSize) {
errno = 1030;
return NULL;
}
// Translate: subtract physical base offset, add window base
return (void *)(gUserMappingBase + (pa - physBase));
}
Core Read/Write Operations
All operations use memcpy with data memory barriers for ordering:
// Physical read: copies from physical address to user buffer
int physrw_physreadbuf(uint64_t pa, void *outBuf, size_t len) {
void *ua = physrw_phystouaddr(pa);
if (!ua) return -1;
dmb sy; // Data memory barrier
memcpy(outBuf, ua, len); // Read from mapped window
dmb sy;
return 0;
}
// Physical write: copies from user buffer to physical address
int physrw_physwritebuf(uint64_t pa, const void *inBuf, size_t len) {
void *ua = physrw_phystouaddr(pa);
if (!ua) return -1;
dmb sy;
memcpy(ua, inBuf, len); // Write to mapped window
dmb sy;
return 0;
}
The physrw_physaccess_mapped() function provides direct pointer access for custom manipulation:
uint64_t pa = 0x3000abcd;
physrw_physaccess_mapped(pa, 0x1000, ^(void *ua) {
uint32_t *val = (uint32_t *)ua;
*val ^= 0xDEADBEEF; // In-place bit manipulation
});
PTE Mapping: The Fallback physrw Implementation
When a full direct window is impossible—such as on iOS 26+ SPTM devices with stricter address-space restrictions—Dopamine falls back to physrw_pte.c. This page-table-entry method maps only the pages actually accessed.
The Magic Page Table Architecture
The PTE implementation allocates a "magic page table" at MAGIC_PT_ADDRESS in user-space. The core mechanism in acquire_window() works as follows:
// Simplified from physrw_pte.c
void acquire_window(uint64_t pa, size_t len, void (^block)(void *)) {
pthread_mutex_lock(&gLock); // Thread-safe access
// Insert temporary PTE entry for target physical page
uint64_t *pte = find_free_pte_entry();
*pte = (pa & ~0x3FFF) | PTE_VALID | PTE_RW | PTE_ATTR_INDEX;
flush_tlb(); // Invalidate TLB if needed
// Calculate user-space pointer into mapped window
void *ua = (void *)(MAGIC_PT_BASE + (pa & 0x3FFF));
block(ua); // Execute caller's code
// Entry persists until explicitly cleared or reused
pthread_mutex_unlock(&gLock);
}
TLB Flush Strategies
The flush_tlb() function forces translation lookaside buffer invalidation through two approaches:
- Older iOS versions: Temporarily overwrite the ASID (Address Space ID) to trigger TLB invalidation
- Newer iOS versions: Busy-wait loop when ASID manipulation is restricted
Thread Safety with Mutex Protection
The PTE method uses pthread_mutex_t gLock to protect the magic page table. This ensures concurrent calls from multiple threads don't corrupt temporary entries—a critical requirement for multi-threaded jailbreak components.
Runtime Initialization and API Unification
Dopamine unifies both implementations behind a common interface through gPrimitives, defined in primitives.h.
Initialization Entry Points in main.c
// libjailbreak_physrw_init() - Direct mapping path
int libjailbreak_physrw_init(void) {
int err = physrw_handoff(); // Establish direct window
if (err) return err;
// Register function pointers
gPrimitives.physreadbuf = physrw_physreadbuf;
gPrimitives.physwritebuf = physrw_physwritebuf;
gPrimitives.physaccess_mapped = physrw_physaccess_mapped;
return 0;
}
// libjailbreak_physrw_pte_init() - PTE fallback path
int libjailbreak_physrw_pte_init(bool useAsync, uint64_t *asidPtrOut) {
int err = physrw_pte_handoff(useAsync, asidPtrOut);
if (err) return err;
// Register PTE-based function pointers
gPrimitives.physreadbuf = physrw_pte_physreadbuf;
gPrimitives.physwritebuf = physrw_pte_physwritebuf;
gPrimitives.physaccess_mapped = physrw_pte_physaccess_mapped;
return 0;
}
Using the Unified API
After initialization, code uses gPrimitives without knowing which implementation backs it:
// Simple physical read
uint64_t target_pa = 0x12345678;
uint8_t buffer[64];
int err = gPrimitives.physreadbuf(target_pa, buffer, sizeof(buffer));
// Physical page write (64 KiB)
uint64_t pa = 0x20000000;
uint8_t data[0x10000];
memset(data, 0xAA, sizeof(data));
int rc = gPrimitives.physwritebuf(pa, data, sizeof(data));
Key Source Files and Their Roles
| File | Purpose |
|---|---|
BaseBin/libjailbreak/src/physrw.c |
Direct-mapping implementation with full physical window |
BaseBin/libjailbreak/src/physrw.h |
Public API and macros for direct mapping |
BaseBin/libjailbreak/src/physrw_pte.c |
Per-page PTE mapping for constrained devices |
BaseBin/libjailbreak/src/physrw_pte.h |
PTE implementation headers |
BaseBin/libjailbreak/src/main.c |
Runtime selection and initialization |
BaseBin/libjailbreak/src/primitives.h |
gPrimitives structure definition |
Summary
-
Direct mapping (
physrw.c) provides zero-overhead access by mapping the entire kernel physical space into user-space, usingphysrw_phystouaddr()for address translation anddmb sybarriers for memory ordering. -
PTE mapping (
physrw_pte.c) enables operation on restricted devices through temporary page-table entries atMAGIC_PT_ADDRESS, protected bypthread_mutex_t gLock. -
Unified interface via
gPrimitivesallows runtime selection without changing caller code—libjailbreak_physrw_init()for direct,libjailbreak_physrw_pte_init()for PTE fallback. -
Address validation (
errno = 1030) and memory barriers appear in both paths for safety. -
iOS 26+ SPTM compatibility requires the PTE method due to stricter address-space limitations.
Frequently Asked Questions
What is physrw in Dopamine?
physrw (physical read/write) is Dopamine's subsystem for accessing kernel physical memory from user-space. According to the opa334/Dopamine source code, it consists of two implementations in BaseBin/libjailbreak/src/: a direct-mapping approach for full physical window access, and a PTE-based fallback for devices with memory protection restrictions.
When does Dopamine use the PTE method instead of direct mapping?
The PTE method activates when the direct mapping cannot cover the kernel physical address space—specifically on iOS 26+ SPTM devices with stricter address-space limits. The initialization code in main.c attempts direct mapping first; if physrw_handoff() fails or is skipped, libjailbreak_physrw_pte_init() prepares the magic page table alternative.
How does physrw ensure memory access ordering?
Both implementations use data memory barriers (dmb sy) surrounding memcpy operations. In physrw.c, physrw_physreadbuf() and physrw_physwritebuf() issue dmb sy before and after memory access. The PTE implementation similarly barriers around the block execution in acquire_window().
What is PPLRW_USER_MAPPING_OFFSET and why does it matter?
PPLRW_USER_MAPPING_OFFSET is a calculated base address (L1_BLOCK_SIZE * L1_BLOCK_COUNT - 0x3000000000) where Dopamine maps the physical window. As noted in lines 87‑93 of physrw.c, this places the mapping beyond first-level translation limits, preventing address-space-overflow crashes on newer iOS hardware with extended physical addressing.
Can multiple threads use physrw simultaneously?
The direct mapping implementation is inherently thread-safe for reads and writes to distinct addresses. The PTE method explicitly protects the magic page table with pthread_mutex_t gLock, serializing access to the temporary page entries. However, concurrent operations to the same physical page require external synchronization.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →