XNU Videoconsole on iOS

Useful for this article

Introduction

Apple’s XNU kernel that is used in their Operating Systems such as macOS and iOS has a console feature: The video console.
Whenever you boot the kernel and on any platform this console is at least present at this time.
It is unknown if Apple is planning to remove the videoconsole from iOS in the future, but it can be considered a very cool and handy feature.

The videoconsole can be found in Platform Expert but is also in the kernel itself.
By default this feature is disabled on production devices, just like serial output over UART or to a terminal.
With the help of a jailbreak the kernel can be patched to enable it again, but due to kernel integrity enforcements by Apple this will not be an easy process.
Therefore I decided to research how to re-implement the video console.

To the initialization roots of the videoconsole

The first notable structure related to video output in XNU is PE_state.

pexpert/pexpert/pexpert.h:

typedef struct PE_state {
                        boolean_t   initialized;
                        PE_Video    video;
                        void        *deviceTreeHead;
                        void        *bootArgs;
                    } PE_state_t;
                    

As you can see this structure has a member video which reveals the more interesting structure PE_Video.

pexpert/pexpert/pexpert.h:

struct PE_Video {
                        unsigned long   v_baseAddr;     /* Base address of video memory */
                        unsigned long   v_rowBytes;     /* Number of bytes per pixel row */
                        unsigned long   v_width;        /* Width */
                        unsigned long   v_height;       /* Height */
                        unsigned long   v_depth;        /* Pixel Depth */
                        unsigned long   v_display;      /* Text or Graphics */
                        char        v_pixelFormat[64];
                        unsigned long   v_offset;       /* offset into video memory to start at */
                        unsigned long   v_length;       /* length of video memory (0 for v_rowBytes * v_height) */
                        unsigned char   v_rotate;       /* Rotation: 0:normal, 1:right 90, 2:left 180, 3:left 90 */
                        unsigned char   v_scale;        /* Scale Factor for both X & Y */
                        char        reserved1[2];
                        #ifdef __LP64__                 /* if 64-bits pointers and long values */
                            long        reserved2;
                        #else
                            long        v_baseAddrHigh;
                        #endif
                    };
                    typedef struct PE_Video       PE_Video;
                    

And here we see the actual important things that we need to alter for the videoconsole to work.
The comments here explain pretty much how the structure is designed already:

  • A base address to the actual video memory is kept.
  • The width and height of the screen are noted.
  • There are two modes for video usage: graphics and text.
  • Scaling and rotation are featured.

Now to continue the research I wanted to make sure that the base address of the video memory is actually being stored in this structure, as a chance exist that is disabled as well but is unlikely due to the bootlogo probably being painted using the video graphics mode.
Therefore I decided to look for the use of PE_video.v_baseAddr and found this in the initialization stage of platform expert.

pexpert/arm/pe_init.h:

void
                    PE_init_platform(boolean_t vm_initialized, void *args)
                    {
                        DTEntry         entry;
                        unsigned int    size;
                        void          **prop;
                        boot_args      *boot_args_ptr = (boot_args *) args;
                    
                        if (PE_state.initialized == FALSE) {
                            PE_state.initialized = TRUE;
                            PE_state.bootArgs = boot_args_ptr;
                            PE_state.deviceTreeHead = boot_args_ptr->deviceTreeP;
                            PE_state.video.v_baseAddr = boot_args_ptr->Video.v_baseAddr;
                            PE_state.video.v_rowBytes = boot_args_ptr->Video.v_rowBytes;
                            PE_state.video.v_width = boot_args_ptr->Video.v_width;
                            PE_state.video.v_height = boot_args_ptr->Video.v_height;
                            PE_state.video.v_depth = (boot_args_ptr->Video.v_depth >> kBootVideoDepthDepthShift) & kBootVideoDepthMask;
                            PE_state.video.v_rotate = (boot_args_ptr->Video.v_depth >> kBootVideoDepthRotateShift) & kBootVideoDepthMask;
                            PE_state.video.v_scale = ((boot_args_ptr->Video.v_depth >> kBootVideoDepthScaleShift) & kBootVideoDepthMask) + 1;
                            PE_state.video.v_display = boot_args_ptr->Video.v_display;
                            strlcpy(PE_state.video.v_pixelFormat, "BBBBBBBBGGGGGGGGRRRRRRRR", sizeof(PE_state.video.v_pixelFormat));
                        }
                        
                        ...
                    
                    

From that structure we now know that iOS uses the RGB (red/green/blue) pixel format.
We can also see that the base address and other video information is read from the boot_args structure.

pexpert/arm64/boot.h:

typedef struct boot_args {
                        uint16_t        Revision;           /* Revision of boot_args structure */
                        uint16_t        Version;            /* Version of boot_args structure */
                        uint64_t        virtBase;           /* Virtual base of memory */
                        uint64_t        physBase;           /* Physical base of memory */
                        uint64_t        memSize;            /* Size of memory */
                        uint64_t        topOfKernelData;    /* Highest physical address used in kernel data area */
                        Boot_Video      Video;              /* Video Information */
                        uint32_t        machineType;        /* Machine Type */
                        void            *deviceTreeP;       /* Base of flattened device tree */
                        uint32_t        deviceTreeLength;   /* Length of flattened tree */
                        char            CommandLine[BOOT_LINE_LENGTH];  /* Passed in command line */
                        uint64_t        bootFlags;      /* Additional flags specified by the bootloader */
                        uint64_t        memSizeActual;      /* Actual size of memory */
                    } boot_args;
                    

Not to overcomplicate things, but that structure contained the Boot_Video structure as seen earlier in PE_init_platform.

pexpert/arm64/boot.h:

struct Boot_Video {
                        unsigned long   v_baseAddr; /* Base address of video memory */
                        unsigned long   v_display;  /* Display Code (if Applicable */
                        unsigned long   v_rowBytes; /* Number of bytes per pixel row */
                        unsigned long   v_width;    /* Width */
                        unsigned long   v_height;   /* Height */
                        unsigned long   v_depth;    /* Pixel Depth and other parameters */
                    };
                    
                    #define kBootVideoDepthMask     (0xFF)
                    #define kBootVideoDepthDepthShift   (0)
                    #define kBootVideoDepthRotateShift  (8)
                    #define kBootVideoDepthScaleShift   (16)
                    
                    #define kBootFlagsDarkBoot      (1ULL << 0)
                    
                    typedef struct Boot_Video   Boot_Video;
                    

Alright we now know how the video information looks like but we want to know where, how and if it is actually initialized from the boot_args structure as one thing known on iOS is that one cannot set boot-args since they will be ignored by the bootloader and not passed to the kernel.
For this we trace the calls to PE_init_platform which makes us end up in the arm initialization stage as part of the Operating System Framework (osfmk).

osfmk/arm/arm_init.c:

void
                    arm_init(
                        boot_args   *args)
                    {
                        unsigned int    maxmem;
                        uint32_t        memsize;
                        uint64_t        xmaxmem;
                        thread_t        thread;
                        processor_t     my_master_proc;
                    
                        // rebase and sign jops
                        if (&__thread_starts_sect_end[0] != &__thread_starts_sect_start[0])
                        {
                            uintptr_t mh    = (uintptr_t) &_mh_execute_header;
                            uintptr_t slide = mh - VM_KERNEL_LINK_ADDRESS;
                            rebase_threaded_starts( &__thread_starts_sect_start[0],
                                                    &__thread_starts_sect_end[0],
                                                    mh, mh - slide, slide);
                        }
                    
                        /* If kernel integrity is supported, use a constant copy of the boot args. */
                        const_boot_args = *args;
                        BootArgs = args = &const_boot_args;
                    
                        cpu_data_init(&BootCpuData);
                    
                        PE_init_platform(FALSE, args); /* Get platform expert set up */
                    
                        ...
                    

That code doesn’t show how the boot_arguments are initialized yet, but it does show that with kernel integrity enabled the boot_arguments can not be patched by a jailbreak.
However, when they later are passed to PE_init_platform that does not matter anymore as we can simply pass the structure using a fake one crafted in the kernel_heap. :)

Lets continue to look for the initialization of these arguments by looking for calls to arm_init().
It seems like it gets slightly more complex at this point, as the only call to it is in start.s, which contains ARM64 assembly instructions and macros.
Because this code is fairly complex to read and no parts of it can be ommited but it is clear that in

osfmk/arm64/start.s

The bootargs are loaded from physical memory, somewhere at the end of the DATA segment of the kernel and eventually passed to cpu_init() after the initialization of the MMU, pagemap, kernel integrity, WatchTower (if supported), exception vector etc.
That indicates that the bootloader passes these boot-arguments thus the video memory base address is mostlikely either passed by it or not passed at all.
Luckily the source code of iBoot of iOS 10 got leaked so I decided to verify this and continue my research there.
The following code snippet is taken from that leak and proofs my theory:

lib/macho/kernelcache.c:

    ...
                        
                        void update_display_info(boot_args *boot_args)
                        {
                            struct display_info info;
                        
                            memset(&info, 0, sizeof(info));
                            display_get_info(&info);
                        
                            boot_args->video.v_rowBytes = info.stride;
                            boot_args->video.v_width = info.width;
                            boot_args->video.v_height = info.height;
                            boot_args->video.v_depth = info.depth;
                            boot_args->video.v_baseAddr = (addr_t)info.framebuffer;
                            boot_args->video.v_display = 1;
                        }
                        
                        ...
                    

So to get an indication of what happens here: The boot-args of the kernel are simply set using a structure display_info that is populated through display_get_info().

lib/paint/paint.c:

...
                    
                    /* after the fact get the information about the current display */
                    int display_get_info(struct display_info *info)
                    {
                        uint32_t rotate = ((360 + mib_get_s32(kMIBTargetOsPictureRotate)) / 90) % 4;
                        uint32_t os_picture_scale = mib_get_u32(kMIBTargetOsPictureScale);
                        uint32_t depth;
                    
                        if (paint_ready) {
                            info->stride = window_stride;
                            info->width = window_width;
                            info->height = window_height;
                            depth = (paint_canvas->cs == CS_ARGB8101010) ? 30 : window_depth;
                            info->depth = depth | (rotate << 8) | ((os_picture_scale - 1) << 16);
                            info->framebuffer = window_framebuffer;
                        } else {
                            info->stride = 640 * 4;
                            info->width = 640;
                            info->height = 1136;
                    
                            /* 
                             * First setting scale as workaround for <rdar://problem/11342009>.  
                             * This lets us use the display properly even if the iBoot display system 
                             * is not active (e.g. fastsim support). 
                             */
                            info->depth = 32 | ((os_picture_scale - 1) << 16);
                            info->framebuffer = (uintptr_t)mib_get_addr(kMIBTargetDisplayBaseAddress);
                        }
                    
                        return 0;
                    }
                    
                    ...
                    

As previously seen the info->framebuffer holds the address of the video memory base that eventually will be in the Boot_Video structure and used by the video_console.
And what also comes clear here is that by default it gets the base address from the kMIBTargetDisplayBaseAddress constant.
Case closed: The video console’s video memory base address is proven to be saved in the boot_args by the bootloader.
Just to cure my curiosity I had to look what the actual base address is, so I traced that to the root as well.

include/lib/mib_def.h:

...
                    
                    #ifdef DISPLAY_BASE
                    MIB_CONSTANT(kMIBTargetDisplayBaseAddress,  kOIDTypeAddr,   DISPLAY_BASE);
                    #endif
                    
                    ...
                    

Sure, lets take a look what this DISPLAY_BASE is because that is basically what we see here, the kMIBTargetDisplayBaseAddress is defined as an address-typed mib constant which has the value of DISPLAY_BASE.
Ofcourse this base address will be different accross platforms, but I do my research on the N71AP which has an S8000 platform, but more precisely it corresponds to the S5L8960X platform.
In the memmap.h file of that platform a nice comment exists explaining the layout of the memory in a table.
It seems that the display framebuffer is based just before iBoot’s heap memory.

platform/s5l8960x/include/platform/memmap.h:

...
                    #define SDRAM_BASE      (0x800000000ULL)
                    ...
                    #define SDRAM_END       (SDRAM_BASE + SDRAM_LEN)
                    ...
                    /* reserved for ASP */
                    // NOTE ASP_SIZE is now defined by the platform or target makefile.
                    #define ASP_BASE        (SDRAM_END - (ASP_SIZE))
                    
                    // Reserved for TZ1/AP Monitor
                    #define TZ1_SIZE        (0x00100000ULL)
                    #define TZ1_BASE        (ASP_BASE - TZ1_SIZE)
                    
                    #define CONSISTENT_DEBUG_SIZE   (0x00004000ULL)
                    #define CONSISTENT_DEBUG_BASE   (TZ1_BASE - CONSISTENT_DEBUG_SIZE)
                    
                    /* reserved area for sleep token info */
                    #define SLEEP_TOKEN_BUFFER_SIZE (0x00001000ULL)
                    #define SLEEP_TOKEN_BUFFER_BASE (CONSISTENT_DEBUG_BASE - SLEEP_TOKEN_BUFFER_SIZE)
                    
                    /* reserved area for panic info */
                    #define PANIC_SIZE      (0x00080000ULL)
                    #define PANIC_BASE      (SLEEP_TOKEN_BUFFER_BASE - PANIC_SIZE)
                    
                    /* reserved area for display */
                    // DISPLAY_SIZE comes from platform's rules.mk
                    #define DISPLAY_BASE        (PANIC_BASE - DISPLAY_SIZE)
                    ...
                    

Quite a lot of relativity going on here, but let me cover the effort of calculating the base address.
platform/s5l8960x/rules.mk:

# Platform target can override any of these sizes by specifying in target config file (apps/iBoot/$target-config.mk)
                    ifeq ($(ASP_SIZE),)
                     ASP_SIZE       := 8*1024*1024
                    endif
                    ifeq ($(TZ0_SIZE),)
                     TZ0_SIZE       := 4*1024*1024
                    endif
                    ifeq ($(DISPLAY_SIZE),)
                     DISPLAY_SIZE       := 16*1024*1024
                    endif
                    

In other words:

ASP_SIZE = 8MB (0x800000)
TZ0_SIZE = 4MB (0x400000)
DISPLAY_SIZE = 16MB (0x1000000)

Unless other options are specified in apps/iBoot/$target-config.mk
It seems like the iPhone 6S is missing there but its assumable that the iphone6-config-base.mk is used because the same SoC platform is defined there.

apps/iBoot/config/iphone6-config-base.mk:

    ...
                        # Override platform default memory map
                        TZ0_SIZE        :=  6*1024*1024
                        ...
                    

As you can see it was still important to look at that file because the TZ0_SIZE is overwritten.
That means that the variables needed for calculating the base address are as following:

SDRAM_LEN = 1GB (0x40000000) (According to platform/s5l8960x/rules.mk)
ASP_SIZE = 8MB (0x800000)
TZ0_SIZE = 6MB (0x600000)
DISPLAY_SIZE = 16MB (0x1000000)

Let’s do the math!


                    SDRAM_END = 0x800000000 + 0x40000000 = 0x840000000
                    
                    ASP_BASE = SDRAM_END - ASP_SIZE
                    = 0x840000000 - 0x800000 
                    = 0x83f800000
                    
                    TZ1_BASE = ASP_BASE - TZ1_SIZE 
                    = 0x83f800000 - 0x00100000 
                    = 0x83f700000
                    
                    CONSISTENT_DEBUG_BASE = TZ1_BASE - CONSISTENT_DEBUG_SIZE 
                    = 0x83f700000 - 0x00004000 
                    = 0x83f6fc000
                    
                    SLEEP_TOKEN_BUFFER_BASE = CONSISTENT_DEBUG_BASE - SLEEP_TOKEN_BUFFER_SIZE  
                    = 0x83f6fc000 - 0x00001000 
                    = 0x83f6fb000
                    
                    PANIC_BASE = SLEEP_TOKEN_BUFFER_BASE - SLEEP_TOKEN_BUFFER_SIZE 
                    = 0x83f6fb000 - 0x00080000 
                    = 0x83f67b000
                    
                    // And finally we can calculate the DISPLAY_BASE
                    DISPLAY_BASE  = PANIC_BASE - DISPLAY_SIZE 
                    = 0x83f67b000 - 0x1000000 
                    = 0x83e67b000
                    
                    // Which means it is ranging from 0x83e67b000 to 0x83f67b000
                    

The functionality of the videoconsole

As I strive to write this document as useful and detailed as possible this part of the article will be released in September, 2019.

Patching the kernel to re-enable the videoconsole

As I strive to write this document as useful and detailed as possible this part of the article will be released in September, 2019.

Conclusion

XNU features a videoconsole that exists on iOS as well but is disabled by default.
The videoconsole is build upon a physical r/w memory range that is defined in iBoot and stored by iBoot in the kernel boot-args.
Re-Enabling the videoconsole is possible without patching data in the Kernel’s protected Read-Only region.
Re-Implementing the videoconsole is relatively easy with minor patches and function calls due to most functions not being stripped and having symbols.
To avoid glitches, one would patch BackBoardd to make it stop using the display.

< back to posts