nubia RedMagic 9 Pro / 9 Pro+ / 9S Pro / 9S Pro+ (NX769J)
nubia RedMagic 9 Pro / 9 Pro+ / 9S Pro / 9S Pro+ (NX769J) on Qualcomm SM8650, targeting iOS 16.5 (build 20F66, Darwin 22.5.0, XNU 8796) with an iPhone X identity. The board shares the SM8550 SoC register map and owns its CPU topology, UFS root policy, Goodix touch input and Adreno 750 GPU work. This record states the implementation coverage of each area and the evidence each milestone requires.
Source record: porting.toml, 2a265afdbd95d45e4a90664157e793c3bfee3755.
This page is generated from the device record.
| Area | Status |
|---|---|
| Configuration | host-verified |
| Firmware and boot | host-verified |
| CPU and interrupts | implemented |
| Memory and DMA | implemented |
| Storage and root | implemented |
| Display | implemented |
| Input | planned |
| GPU | planned |
| Network | planned |
| Audio | planned |
| Power | planned |
| Image assembly | host-verified |
| Diagnostics | planned |
Configuration
host-verified
Components: ArmDTPlatform, SoftwareAES, XnuPrelink.
Hardware and dependencies
- SM8650 register map and CPU topology from mainline sm8650.dtsi.
- iOS 16.5 build 20F66 OS profile with XNU 8796.101.5.
- UniXNU-sm8550-common revision 23b2049fcd9e30356161def84231334fb55ecca9 as the SoC source.
- Board identity from the NX769J Android 14 vendor firmware and board configuration.
Configuration
- board.name = nx769j; board.soc = SM8650; board.soc_source = sm8550; board.os_profile = ios-16.5-20F66; schema_version = 2.
- components.include lists the complete selection; arrays replace the inherited ARM64, SoC and OS arrays.
- kernel.defconfig = nx769j_defconfig; config_fragments apply arch, soc, os in order and the device defconfig last.
- device_tree.source = device:dts/nx769j.dts with include_dirs = [soc:dts].
- Board submodules are not used; every driver comes from the component catalog at its pinned revision.
Procedure
- Read https://unixnu.com/porting/device-repository and https://unixnu.com/porting/records.
- Run ./unixnu defconfig nx769j_defconfig -D ../UniXNU-NX769J from the UniXNU checkout.
- Run ./unixnu fetch -D ../UniXNU-NX769J and commit the resulting unixnu.lock.
- Run ./unixnu status --json -D ../UniXNU-NX769J and review the resolved profiles, components, source pins and patch order, then ./unixnu tree -D ../UniXNU-NX769J.
Development findings
- Tables merge in ARM64, SoC, OS, device order and arrays replace inherited arrays, so the device must list every required component and fragment.
- board.toml sets bootimg.image.header_version to 0 rather than the 4 the NX769J board configuration records. unixnu bootimg verify() reads the header at the v0/v1/v2 offsets and would reject a v4 image, and the tool defaults use 0. The reasoning and the open question about what the NX769J ABL accepts are recorded in the packaging area.
- soc:configs/sm8550.config sets CONFIG_XNU_NATIVE_ADRENO=y. The device defconfig applies last and sets CONFIG_XNU_NATIVE_ADRENO=n because the SM8550 GPU stack targets Adreno 740 while this board has Adreno 750.
- There is no UniXNU-sm8650-common. sm8550-common is usable as the SoC source because the register map of every resource this port drives is identical on both parts: GICD 0x17100000/0x10000, GICR 0x17180000/0x200000, UFS host 0x01d84000/0x3000, UFS PHY 0x01d80000/0x2000, GPU 0x03d00000/0x40000, GMU 0x03d6a000/0x35000 and GPU SMMU 0x03da0000/0x40000. The CPU core names and the Adreno generation do differ and are overridden in the board DTS and defconfig. The device confirms the UFS ranges in /proc/iomem and the MPIDR layout in its CPU nodes.
- board.os_profile selects a directory under UniXNU/profiles/os/, and this port targets ios-16.5-20F66, which does not exist in UniXNU yet. The profile is kept in this repository at os-profiles/ios-16.5-20F66/ and the Build workflow installs it into the UniXNU checkout before resolving. The [ipsw] and [kernel.prelink] tables are inherited from that profile rather than repeated here, so the REQUIRED_IPSW fields are filled in one file.
- board.toml selects only device:patches/xnu/series. The shared arch, soc and os series still target xnu-10063.141.1 while this port runs xnu-8796, so referencing them would either abort unixnu status with 'patch missing' or fail during application. The rebased copies are carried here and the four-series form goes back once the shared series are rebased.
- unixnu status resolves the complete ordered patch series, so it needs the source of every component named by a component: entry. The 17.7.11 OS series names XnuPrelink twice and status aborts with 'patch missing' when that source is absent, while defconfig alone succeeds because it only reads the configuration fragments. The resolve build test therefore runs fetch and tree before status, which is the step 4 order in the porting guide.
- unixnu fetch records component sources in unixnu.lock as well as feed sources when the device repository declares no git submodules. UniXNU-sheng pins its board drivers as submodules and its lock carries feed sources only; this repository carries both, and every component revision in the lock matches its pin in the component catalog.
- unixnu.lock is machine written: fetch rewrites it with a single header comment and drops any comment added by hand. Keep explanations out of the lock and record them here, so the file stays byte-identical to what fetch produces. The profile's explicit xnu_revision takes priority over the lock, so the xnu entry follows the OS profile.
- The default install path writes the OS windows into userdata, which is Android's /data and leaves the system images intact but the stock install unusable. The dual-system partition tool instead carves a unixnu partition from the end of userdata, preserving userdata's start, type, GUID and attributes and leaving metadata, super and boot untouched. README.md records the four-step order that ends in adb reboot system, which is the check that the stock system still boots.
- The dual-system partition tests are documented for Linux, WSL and macOS. On a native Windows host the four RecoveryTests fail because they drive twrp.sh through a POSIX sh and a colon PATH, so they run in the partition-safety build job on ubuntu-latest instead. The seven layout tests that cover GPT entry and GUID preservation, reserved GPT space and F2FS section rounding pass on either host.
- The board hardware values that no public source provides were read from a physical device over read-only adb and recorded in device-inputs.md: the userdata GPT extents and block size, the splash reservation, the DRAM base and the panel geometry. The handset reports a spoofed Xiaomi identity through its userspace properties while its boot image, kernel and device tree identify NX769J, so the values came from the kernel, the device tree and the raw UFS partition table rather than from getprop.
- The kernel build runs in CI on ubuntu-latest with the UniXNU Linux toolchain, upstream clang and an open source Mach-O toolchain rather than Xcode. The toolchain is expensive to build and is cached under /opt/unixnu-linux keyed on scripts/linux. The job also asserts that the patch order unixnu resolves matches patches/xnu/series exactly, so a reordered or dropped patch fails the build rather than silently changing what gets compiled.
- The toolchain cache never persisted and every kernel run rebuilt clang from source, which is the whole 37 minutes. The kernel job is expected to fail while this port is being brought up and actions/cache's combined form only saves on success, so the entry was never written. save-always had no effect and was replaced with explicit restore and save steps, the save running on always(). Restoring the cache then skipped setup-toolchain.py and with it the apt packages the toolchain links against, so --check failed on missing libclang and libbsd; the host dependencies are now installed in their own step regardless of the cache. The installer itself must also run on a cache hit: it places /usr/bin/xcrun and the CLT shims in system paths the cache cannot hold, and scripts/unixnu/kernel.py requires xcrun, so a cache hit skipped the step and the build stopped at 'xcrun missing'. The installer is idempotent and skips each component that already exists, so running it always costs nothing when the cache hits. Installing under the runner's home rather than /opt is separate hygiene: the installer runs as root and the path is expanded before sudo because sudo resets HOME to /root. scripts/unixnu/kernel.py resolves the toolchain through UNIXNU_TOOLCHAIN, so the same variable drives both the install and the build.
- The kernel step writes its compile errors to logs/kernel-bootstrap.log rather than stdout. Parallel make then interleaves that file so heavily that the diagnostic for the failing compile is garbled past reading, and a failing run was not diagnosable at all. The job uploads out/**/logs as an artifact and repeats the build with -j 1 on failure, so the retry names the actual error. The retry is incremental because the objects from the parallel pass are reused.
- A change under .github/**, unixnu.lock or os-profiles/** is owned here or in the packaging area, so such a change has to land with a matching finding or evidence entry in the owning area to satisfy the record checker's changed-area rule.
- payload.adt.root_match_volume_uuid is now filled, so [payload.adt] has no REQUIRED_ placeholder left. It is the APFS volume UUID of the OS window, 97A3EB1A-5218-4936-BC85-9574EC2C8CC9, and not the container UUID c155fa03-68f4-42b0-8117-1915b6aa8e4c that sits in the same superblock: /chosen/root-matching matches IOMedia, and the container UUID belongs to the synthesized whole-disk IOMedia rather than to anything that mounts. scripts/extract-root-volume-uuid.py reads the value out of the userdata image the build writes, so it needs no device and no macOS.
- The Data seed workflow puts its diagnostics in the job summary rather than only in the log: the runner's free disk before the download, the disk again once the restore image is deleted, and the seed report. Job logs need repository access to read, so a summary plus an ERR trap in every run step is what makes a failed run diagnosable from the run page: the trap prints ::error title=<step>::<command> and the annotations block is public. The 6 GiB restore image is not cached either -- a cache entry that size doubles the disk peak of a step that is already holding the archive and its 4.7 GiB OS member, and the archive is deleted as soon as the member is out. The Data seed workflow ran twice and failed twice before it was right: cache keys reject commas and the product type iPhone10,3 was in the first key, and the seed script then made a 32 MiB container and grew it, which is both an extra round of attach and resize and a size the iOS 16.5 var tree may not fit into. It now creates the container at its target size and refuses up front when the tree would not fit. [payload] tools = true adds a Tools entry to the generated boot menu, which is where the firmware log exporter publishes its QR pages. The generated menu replaces the built-in one and with it its Restart and Power off, so payload.py adds those back; package/unixnu.toml is a hand-written menu and is not the one the payload carries. .github/workflows/data-seed.yml is the macOS runner job that builds the Data seed, and it reads its restore-image pins out of os-profiles/ios-16.5-20F66/profile.toml so the workflow and the profile cannot drift apart.
Completion criteria
- All source pins resolve; every selected component has a record; status shows the intended overrides and patch order.
Current limits
- local.toml can select a different source from the published pin; record the effective source in each verification result.
- The SoC source is the SM8550 repository. A dedicated UniXNU-sm8650-common would carry the SM8650 CPU topology and Adreno 750 platform operations instead of board-level overrides.
Next action
- Compare the effective status output whenever a profile, source or component selection changes.
- Extract the SM8650 CPU topology and platform operations into UniXNU-sm8650-common once the board needs GPU power votes.
Evidence
source: Board configuration, component selection, defconfig and patch order described in this section; [firmware.defines] for the EDK II build, and [payload.adt].root_match_volume_uuid, which is the APFS volume UUID of the OS window, 97A3EB1A-5218-4936-BC85-9574EC2C8CC9, read out of the volume superblock of the built userdata image by scripts/extract-root-volume-uuid.py. Note the other UUID in the same container: c155fa03-68f4-42b0-8117-1915b6aa8e4c is the container's nx_uuid and is what some tools label the APFS volume ID, but it sits at block+0x48 of the container superblock and matches the synthesized whole-disk IOMedia rather than anything that mounts. firmware.py drives EDK II build directly and so bypasses Mu-Silicium's build driver; an undefined ENABLE_SECUREBOOT reaches SecureBootHandlerLib as an empty token and fails the preprocessorsource: SoC source URL and reviewed commit pinning the SM8550-common revisionsource: Measured board inputs with the read-only adb commands that produced themsource: The Build workflow sets CCACHE to scripts/compile-records.py for the kernel step, so the per-object compile recipes the archive builders replay are produced on CI as they are locally, and reports how many were writtensource: The [kernel] archives list drops build-mockfs-fixture.py, which the kernel only force-loads when MOCKFS_MODE is 1, and [firmware] gains the full uefi-nx769j platform package tablehost: 2026-10-30: the resolve step ran to completion and reported the 64 patches of patches/xnu/series in order with every selected component at its pinned revision. python3 scripts/porting-record.py check --device ../UniXNU-NX769J then reported 13 structured areas checked, and scripts/check-repository.py reported the repository structure consistent. Command:./unixnu status --json -D ../UniXNU-NX769J. Revision:dd97bc4b307f77df2f6189b7c92881133cc24ac3.
Firmware and boot
host-verified
Components: DarwinBoot.
Hardware and dependencies
- uefi-nx769j Mu-Silicium platform package for the Pineapple QRD base board.
- Stock firmware DTB carried in the device vendor_boot image.
- UEFI GOP handoff, PSCI SMC interface and the vendor firmware inventory from proprietary-files.txt.
- Android 14 ABL: boot header version 4, 4096-byte pages, kernel base 0x0.
Configuration
- firmware.config = nx769j; firmware.dsc = Platforms/ZTE/nx769jPkg/nx769j.dsc; firmware.fd = Build/nx769jPkg/{target}_CLANGPDB/FV/SILICIUM_UEFI.fd. The vendor directory is ZTE, not Nubia: nubia is ZTE brand and the verified sibling package is Platforms/ZTE/nx729jPkg.
- firmware.preflight = [scripts/check-nx769j-firmware.py], which checks the Mu-Silicium layout, that Resources/Configs/nx769j.toml agrees with board.toml, that the DSC includes the SM8650 SoC package and that the vendor DXE drivers are present.
- firmware.submodules = [Common/Mu, Common/Mu_OEM_Sample, Silicium-ACPI, Silicon/Silicium/OpensslPkg/Library/OpensslLib/openssl]; Mu_Basecore is the separate {edk2} checkout, not a submodule.
- firmware.patches = [Resources/MuPatches/Gic-Spurious.patch], applied to the shared edk2 tree for the build and reverted afterwards.
- firmware.packages = [{uefi}, {uefi}/Platforms/ZTE, {tree}/package, {tree}/drivers/DarwinBoot, {edk2}, {uefi}/Common/Mu, {uefi}/Common/Mu_OEM_Sample, {uefi}/Silicon/Qualcomm, {uefi}/Silicon/Silicium, {uefi}/Silicium-ACPI, {uefi}/Silicium-ACPI/Platforms/ZTE, {uefi}/Silicium-ACPI/Silicon/Qualcomm].
- firmware.defines.DEVICE_MODEL = nx769j.
- The platform DSC includes LanaiPkg/LanaiPkg.dsc.inc with SOC_TYPE = 2. LanaiPkg is Mu-Silicium SM8650 support and SOC_TYPE 2 is SM8650-AB, Snapdragon 8 Gen 3 at 3.30 GHz with 8 cores.
- [bootimg.image] base = 0x0, pagesize = 4096, header_version = 0. The ABL is a version 4 boot chain, and Resources/Configs/nx769j.toml records 4, but bootimg.py merges DEFAULTS, then the firmware config and then board.toml, so board.toml decides what is packed: bootimg.py verify() only reads v0/v1/v2 headers and a v4 image fails its round-trip check. Extending verify() for v4 is the fix if the ABL rejects the v0 image.
Procedure
- Read https://unixnu.com/porting/uefi-firmware and create the uefi-nx769j platform package.
- Include UniXNU, UniXNUPkg, DarwinBoot and, for a Mu-Silicium initrd, UniXNURamDiskDxe, DiskIoDxe and FAT support in the DSC and FDF.
- Include the firmware log exporter from https://unixnu.com/porting/log-capture\#firmware-setup so logs can be scanned from the menu during bring-up.
- Run ./unixnu build firmware -D ../UniXNU-NX769J --uefi ../uefi-nx769j and record the firmware commit, output FD and handoff memory map.
- Run ./unixnu bootimg with the firmware DTB and inspect the payload report.
Development findings
- Mu-Silicium names SoC packages by Qualcomm platform codename, not by part number. NX769J is Lanai: the device firmware reports baseband 5.0.c3-00030-LANAI_GEN_PACK and trustzone LANAIAAAAANAZT, and LanaiPkg.dsc.inc declares SM8650-AA through SM8650-AC together with the GIC bases 0x17100000 and 0x17180000 that this board measures. The KailuaPkg that nx729jPkg includes is SM8550 and is wrong for this board.
- UniXNU/uefi-sheng branch sheng already carries Platforms/ZTE/nx729jPkg, a complete platform package for a nubia RedMagic one SoC generation back whose boot.img reaches fastboot boot OKAY on hardware. Platforms/ZTE/nx769jPkg is the same derivation from the Pineapple QRD base board that nx729jPkg is, so the vendor directory is ZTE rather than Nubia.
- nx729j.toml sets boot_image.header_version = 4 where sheng and qrd8550 use 3 and 0, which confirms the NX769J ABL setting independently of the board configuration.
- The UniXNU boot chain modules are not a firmware-side gap: ./unixnu build package builds UniXNU.efi, UniXNUDxe.efi and UniXNURamDiskDxe.efi on Linux with CLANGPDB, which is what proves the DSC integration points this design specifies resolve. What the firmware step still needs is the vendor binary set.
- The firmware DTB is a separate input to boot-image assembly; XNU consumes the ADT generated from the board DTS.
- NX769J boots through UEFI: the board configuration sets TARGET_USES_UEFI := true and records BOARD_BOOTIMG_HEADER_VERSION := 4. What the ABL does with the version 0 image unixnu packs is unverified; the sibling RedMagic port packs the same v0 image and fastboot-boots it on hardware, which is the evidence that this is the right shape.
- The stock bootloader keeps a contiguous-splash region. It is measured at 0xe3940000 with size 0x2b00000 and label cont_splash_region, and board.toml re-adds it to the boot image's device tree through a [[bootimg.dtb_node]] entry, the way sheng declares /reserved-memory/splash_region.
- The device tree model stays the Qualcomm reference name on this board family. This unit reports 'Qualcomm Technologies, Inc. Pineapple MTP' with compatible qcom,pineapple-mtp and qcom,pineapple, and the NX769J stock DTB overlay in the public dump targets qcom,pineapple-qrd. The board DTS therefore names nubia,nx769j with the qcom,pineapple fallbacks.
- The firmware step now runs end to end. All 52 vendor modules are extracted from uefi.img and laid out with their INF wrappers under Binaries/nx769j, the firmware DTB is extracted from vendor_boot tree 2, and ./unixnu build firmware --uefi ../uefi-nx769j produces Build/nx769jPkg/DEBUG_CLANGPDB/FV/SILICIUM_UEFI.fd at exactly 3145728 bytes, which is [uefi_fd] size and what firmware.py checks. Mu_Basecore has to be initialised as a submodule before that: it is the {edk2} tree the build runs against and it is not in the four submodules firmware.submodules lists. boot.json records the same fd_sha256 that the build produced, so the firmware packed into boot.img is the firmware that was built.
- firmware/ changes do not reach the build until they are lifted into the Mu-Silicium checkout: firmware.py drives EDK II against the checkout's copies and never copies from the device repository, so cp -R firmware/. ../uefi-nx769j/ is a required step and a missed one builds stale sources without any warning. The preflight cannot catch it either, because vendor_drivers() reads the platform Include/ from the device repository when the checkout's is absent. Adding the log exporter and rebuilding without lifting produced a byte-identical-looking FD that simply did not contain the module.
- The firmware log exporter is integrated and is in the firmware volume: AdvancedLogDxe builds as its own FFS, GenFfs names it 8D4E7F50-3A2C-4C9A-9E1B-50C4C8472D41 and FV/FVMAIN.inf lists it, so it rides in the compressed FvMain. board.toml [payload] tools = true adds the Tools entry to the generated menu, which is where it publishes the log. What is not done is step 2 of the log-capture procedure: nothing routes this platform's debug output into the Advanced Logger, so the buffer the exporter reads is whatever LanaiPkg's DebugLib happens to write.
Completion criteria
- Firmware image builds, enters UniXNU, publishes the framebuffer and preserves the documented memory reservations.
Current limits
- Library/MemoryMapLib.c is rebuilt from this board's firmware device tree rather than carried over. Every one of the 36 fixed-address reservations has a descriptor named after its node, and the check over the table reports no overlaps and no uncovered reservation. UEFI's own regions -- FD Reserved, UEFI Stack, SEC Heap, Sched Heap, FV Region, UEFI RESV, Kernel, DXE Heap -- are kept where nx729jPkg puts them, which fits this board's free band 0xa1f80000..0xd7e00000 exactly. Display Reserved moved to the measured splash at 0xE3940000, and DXE Heap now starts at 0xB8000000 because the region nx729j gave to its splash is ordinary RAM here and the device tree reserves nothing in it.
- The vendor UEFI DXE drivers are laid out and the firmware builds. scripts/extract-uefi-drivers.py recovers all 52 that the platform includes name, from uefi.img, where they sit in gzip-compressed nested firmware volumes under an outer volume holding only SEC and a platform configuration. Twelve carry a UI name different from the INF name the DSC uses -- I2CDxe's module is called plain I2C -- and the script maps them rather than guessing by prefix. The expected set is read from APRIORI.inc, DXE.inc and RAW.inc rather than kept in step with them: APRIORI.inc names one module that DXE.inc has commented out. They cannot be redistributed, so every build machine runs the extraction.
- The Silicium-ACPI SoC tables are Kailua's, which is SM8550. Silicium-ACPI has no Lanai set, and APIC, GTDT and PPTT describe Kailua's cores where this board has two A520, five A720 and one X4. XNU reads the Apple device tree rather than ACPI, so this does not affect the XNU boot, but the tables are wrong for an ACPI operating system.
- SOC_TYPE is set to 2 from the board name and the 8-CPU topology. The device does not record the prime core maximum frequency, so 2 against 4 (3.30 against 3.40 GHz) is not measured.
Next action
- Build a Lanai ACPI table set, or drop the tables from the DSC if XNU is the only target. Silicium-ACPI has none and the Kailua tables describe a different CPU topology.
- Read the prime core's maximum frequency from the device and settle SOC_TYPE 2 against 4, which is 3.30 against 3.40 GHz.
- Then reach the UniXNU menu with a published 1116x2480x32 framebuffer, which is the first host-side observation that the firmware runs.
Evidence
source: The uefi-nx769j platform package design: the Lanai SoC selection, every board.toml and DSC value with its provenance, the required UniXNU module integration, the vendor input list and the open itemssource: Device configuration staged for the Mu-Silicium checkout: uefi_fd 0xA7100000 for 0x300000 and the version 4 boot header the NX769J ABL requiressource: Recovers all twenty vendor UEFI DXE drivers from uefi.img: the image's outer volume holds only SEC and a platform configuration, and the modules sit in gzip-compressed nested volumes. Nine carry a UI name different from the INF name the DSC uses, and the script maps themsource: Recovers all 52 vendor UEFI DXE drivers from uefi.img and lays each out with its EDK II INF wrapper and depex under Binaries/nx769j. The outer volume holds only SEC and a platform configuration; the modules are in gzip-compressed nested volumes. Twelve carry a UI name different from the INF name and the script maps themsource: Splits the vendor_boot dtb field into its device trees and prints each one's model, qcom,msm-id and reservations. This is where the memory map data comes from: 49 reserved-memory nodes, 36 at fixed addresses, and the four Pineapple trees byte-identical in that setsource: The board memory map, rebuilt from the firmware device tree: every one of the 36 fixed-address reservations has a descriptor named after its node, UEFI's own regions sit in the free band the device tree leaves open, and the table has no overlaps and no uncovered reservationsource: Platform DSC staged for the Mu-Silicium checkout: LanaiPkg with SOC_TYPE 2, the measured framebuffer geometry and DRAM base, the board SMBIOS identity and the UniXNU and DarwinBoot componentssource: Preflight that firmware.py runs before EDK II: layout, board.toml agreement, the SoC package include, UniXNU module coverage and the vendor DXE inventory. With the extraction done it reports 52/52 present and the tree consistent with board.tomlsource: The flattened-device-tree reader the firmware tools share: the node walk and the reserved-memory extents. extract-firmware-dtb.py and check-memory-map.py both read reservations through it, so the listing and the check cannot drift apartsource: Compares MemoryMapLib.c with the reservations of the firmware device tree and with the ones board.toml [[bootimg.dtb_node]] adds. It refuses an uncovered reservation, two descriptors covering one range, a DDR descriptor outside the DRAM window and a descriptor that covers nothing and is not a UEFI region. Over the built tree: 64 descriptors, 37 fixed reservations, 37 covered exactly once, 0 unexplainedsource: The hash of every extracted vendor module and of the two flash-package images they come from, so a build machine can prove it extracted from the right uefi.img rather than from a different firmwaresource: The firmware log exporter of https://unixnu.com/porting/log-capture#firmware-setup: UniXNUPkg/Driver/AdvancedLogDxe/AdvancedLogDxe.inf in [Components], AdvancedLoggerAccessLib and QrEncoderLib mapped from Common/Mu, and PcdUniXNUDeviceModel set to nx769j so the exported log pages are named after the board. Its Depex is gAdvancedLoggerProtocolGuid, so it loads only when the platform's Advanced Logger is configured and the Tools entry appears thenhost: 2026-10-30: EDK II built Platforms/ZTE/nx769jPkg/nx769j.dsc with CLANGPDB and produced Build/nx769jPkg/DEBUG_CLANGPDB/FV/SILICIUM_UEFI.fd at 3145728 bytes, exactly [uefi_fd] size, sha256 b06906387e20df18f2eef45267ccc11160af4b3ed8e30887a4b1c6d4df7a4fbc. The preflight reported 52/52 vendor drivers present and the tree consistent with board.toml, and AdvancedLogDxe is in the volume: its FFS is 8D4E7F50-3A2C-4C9A-9E1B-50C4C8472D41 and FV/FVMAIN.inf lists it. Nothing here is booted. Command:./unixnu build firmware -D ../UniXNU-NX769J --uefi ../uefi-nx769j. Revision:dd97bc4b307f77df2f6189b7c92881133cc24ac3.
CPU and interrupts
implemented
Components: ArmCpu.
Hardware and dependencies
- SM8650 CPU topology from mainline sm8650.dtsi: cpu0-1 arm,cortex-a520, cpu2-6 arm,cortex-a720, cpu7 arm,cortex-x4.
- MPIDR affinities 0x0, 0x100, 0x200, 0x300, 0x400, 0x500, 0x600, 0x700.
- GICv3 distributor 0x17100000/0x10000, redistributors 0x17180000/0x200000 with #redistributor-regions = 1 and redistributor-stride 0x40000, ITS at 0x17140000/0x40000.
- 19.2 MHz timer. PSCI uses SMC. GIC PPI partitions group cpu0-1, cpu2-6 and cpu7.
Configuration
- dts/nx769j.dts includes sm8550.dtsi and replaces each cpuN compatible with the SM8650 core name, keeping the DTSI MPIDR values.
- unixnu,cluster-type = E on cpu0-1 and P on cpu2-7 feeds the ADT scheduler grouping.
- CONFIG_XNU_ARMCPU=y and CONFIG_XNU_NATIVE_ARCH_TIMER=y from the ARM64 common fragment.
Procedure
- Compile the board DTS and inspect enabled CPUs, GIC ranges and timer properties in the generated ADT.
- Record per-CPU entry, timer interrupts and inter-processor interrupt delivery at the checkpoints in https://unixnu.com/porting/bring-up.
Development findings
- The SM8550 DTSI declares three A510 cores followed by A715, A710 and X3 cores. SM8650 instead has two A520 cores first and a single X4 prime core last, so the compatible strings must be replaced per node while the MPIDR layout is unchanged at 0x100 spacing.
- The GIC PPI partitions in sm8650.dtsi confirm the grouping independently of the core names: partition 0 is cpu0-1, partition 1 is cpu2-6 and partition 2 is cpu7.
- CPU affinity values are hardware identifiers; enabled CPU count and MPIDR affinities must be checked together.
- The device confirms the MPIDR affinities 0x0, 0x100, 0x200, 0x300, 0x400, 0x500, 0x600, 0x700 across its eight cpu@N nodes. Its vendor kernel names the cores qcom,kryo rather than the mainline ARM core names; the board DTS keeps the mainline names that sm8650.dtsi declares and that the UniXNU CPU driver matches on.
Completion criteria
- Every enabled CPU reaches its entry checkpoint and timer/IPI handling works on each enabled CPU.
Current limits
- CPU frequency and idle policy are board inputs and are not implemented; no CpuFreq board service exists yet.
- Per-CPU entry and interrupt delivery are not yet observed on this board.
Next action
- Record per-CPU entry and interrupt observations once the firmware reaches the kernel handoff.
Evidence
source: SM8650 CPU topology, MPIDR affinities, cluster types and GIC handoff described in this sectionsource: Measured MPIDR affinities across the eight cpu@N nodes and the vendor core naming
Memory and DMA
implemented
Hardware and dependencies
- ARM64 Qualcomm DRAM base 0x80000000, confirmed on the device by gunyah_hyp_region@80000000 and the lowest System RAM range.
- Handoff window 0x80000000 bytes at 0x80000000, matching payload.dram_size.
- Measured contiguous-splash reservation at 0xe3940000 with size 0x2b00000, label cont_splash_region.
- Apps SMMU at 0x15000000/0x100000 and GPU SMMU at 0x03da0000/0x40000, both qcom,sm8650-smmu-500.
- UFS host and PHY register ranges shared with SM8550.
Configuration
- device_tree.source = device:dts/nx769j.dts; include_dirs = [soc:dts].
- memory@80000000 declares one contiguous DRAM range; payload.adt.owned_memory_size and payload.dram_size are both 0x80000000.
- reserved-memory declares splash_region at 0xe3940000 with size 0x2b00000 and no-map, and board.toml re-adds the same node to the boot image tree.
Procedure
- Compare firmware RAM ranges, reserved regions, DTS ranges and ADT memory-map entries.
- Check DMA buffers against controller address width, cache policy and SMMU mapping before submitting work.
- Record the measured reservations in device-inputs.md with the command that produced each value.
Development findings
- The memory node describes the contiguous DRAM range handed to XNU, not the installed RAM. This unit reports MemTotal 15705424 kB, about 15.0 GiB usable from a 16 GiB SKU, with System RAM ranges above 4 GiB at 0x880000000, 0x8b0000000, 0x8c0000000 and 0xa00000000. UniXNU takes one contiguous window from that map.
- The handoff window and payload.dram_size are still 2 GiB at 0x80000000, following sheng and caihong. UniXNU-nx729j, a RedMagic 8 Pro+ port built on the same code base, instead passes the full 16 GiB computed from the device ddr-regions 0x80000000/2G plus 0x880000000/6G plus 0xa00000000/8G, and this unit's measured System RAM shows the same three-region shape. The two parameters are independent, so they need deciding together against the firmware memory map before the first kernel boot rather than assumed from either port.
- The splash reservation is the one board value that only the device can give: node name splash_region, label cont_splash_region, reg 0xe3940000 with size 0x2b00000. The node name and label match UniXNU-sheng and the size also matches sheng and shennong, so only the address is board specific.
- Physical register translation and CPU virtual mappings have different purposes. Preserve physical resource ranges in hardware descriptions.
- The Apps SMMU and GPU SMMU register ranges are identical between SM8550 and SM8650, so the shared SMMU kit is applicable to this board.
- An exported register below 16 MiB is an error: the common generator reads the arm-io window base as the SoC base and requires it to be non-zero.
- The kernel memory node is split into System RAM ranges with reservations removed, not the raw DRAM extent. Do not copy it into the board DTS; the board declares the handoff window.
Completion criteria
- All handoff regions are accounted for and each enabled DMA engine accesses its allocated range with the documented cache policy.
Current limits
- The removed regions listed by the firmware are not all enumerated here; only the splash reservation is consumed by the current configuration.
- The handoff window is a build policy value. Confirm it against the firmware memory map before a kernel boot.
Next action
- Compare the firmware memory map against the declared handoff window and the splash reservation at the first kernel boot.
- Record each remaining reserved region that a driver starts consuming.
Evidence
source: DRAM handoff window and reserved-memory node state described in this sectionsource: Measured DRAM base, System RAM ranges and the splash reservation at 0xe3940000 with the commands that produced themsource: The UEFI memory map is now checked against the reservations rather than reviewed. It compares MemoryMapLib.c with the firmware device tree and with the splash board.toml [[bootimg.dtb_node]] adds, and reports 64 descriptors with 37 fixed reservations each covered exactly once and 0 unexplained. scripts/test_check_memory_map.py covers the four ways the map can be wrong
Storage and root
implemented
Components: ShengUfs, VirtioMmioBlock, VerifiedRoot.
Hardware and dependencies
- UFS host 0x01d84000/0x3000 (qcom,sm8650-ufshc, jedec,ufs-2.0, SPI 265) and UFS PHY 0x01d80000/0x2000 (qcom,sm8650-qmp-ufs-phy).
- GCC AXI, AHB, UniPro, symbol and PHY auxiliary branches; power domains and firmware link state.
- Apps SMMU at 0x15000000, UFS stream assignment, context-bank ownership, DMA width and cache maintenance.
- NX769J is an A/B device with dynamic partitions; userdata is a f2fs volume at /dev/block/bootdevice/by-name/userdata and the super partition is 9126805504 bytes.
Configuration
- Select ShengUfs, VirtioMmioBlock and VerifiedRoot; CONFIG_XNU_NATIVE_UFS=y and CONFIG_XNU_NATIVE_VIRTIO_BLOCK=y.
- dts/nx769j.dts sets &ufs and &ufsphy status = okay.
- board.toml [userdata] carries the measured extents: first_lba 3302184, last_lba 124002298, lba_bytes 4096, with OS and SystemOS windows.
Procedure
- Read https://unixnu.com/porting/ufs with this storage record.
- Inspect clock enables, power state, HCE/HCS, link state, request-list base and SMMU mappings before changing controller ownership.
- Validate SCSI capacity and GPT reads, then publish the OS and SystemOS windows within the userdata range.
- Verify persistent Data writes on the documented Data allocation with a recovery image available.
Development findings
- UFS host and PHY addresses are identical between SM8550 and SM8650, and the compatible differs only in the SoC name (qcom,sm8550-ufshc versus qcom,sm8650-ufshc). The shared ShengUfs controller access applies to this board; the board owns its userdata policy. The device confirms both ranges in /proc/iomem.
- An idle AHB branch may report CLK_OFF with hardware gating enabled. Check enable and gating bits together.
- SMMU bypass still has address-width constraints. Validate every DMA range and context-bank assignment.
- recovery.fstab mounts /data as f2fs on userdata, so the persistent Data path must handle f2fs rather than the ext4 layout assumed by some board configurations.
- The measured GPT has userdata as the last occupied partition ending exactly on the last usable sector, with 32 table entries of which 12 are used. Those are the preconditions the dual-system partition tool requires, so a unixnu partition can be carved without altering any other entry.
- The device publishes five regular UFS LUNs plus the RPMB and boot well-known LUNs (49456, 49476, 49488).
- The public NX769J board configurations record BOARD_SUPER_PARTITION_SIZE := 9126805504 with a TODO saying the value is hardcoded. The measured super partition is 12.0 GiB; that guess is not the real size.
Completion criteria
- Host: register, clock, link and SMMU facts match the board inventory.
- Reads: SCSI capacity, GPT and known blocks are correct; OS/SystemOS windows stay within userdata.
- Root: the configured volume mounts and launchd starts. Persistence: writes survive reboot within the allocated Data region.
Current limits
- payload.adt.root_match_volume_uuid is read out of the built userdata image rather than measured on a device, so it is correct for the image that gets flashed and does not follow a repartitioned or differently imaged device.
- ShengUfs contains sheng Data and log policy that is board specific and is not reused here.
- The GPT extents are measured on one unit. Re-read them before writing, since a different SKU or a repartitioned device has different bounds.
Next action
- Confirm the SCSI capacity reported by the controller matches the measured GPT before the first write.
- Produce the owner-signed verified-root policy bundle over the root windows of the OS image that gets flashed and replace the generated empty table in scripts/build-verified-root.py with it, so root verification is enabled rather than disabled.
Evidence
source: UFS host and PHY register ranges, node enables and the A/B userdata layout described in this sectionsource: Measured GPT extents for userdata, the UFS LUN topology and the /proc/iomem confirmation of the controller rangessource: Reads the APFS volume UUIDs out of the userdata image the build writes, attributing each to the window it belongs to by reading userdata-root.json. It finds the containers by their NXSB magic and block size, the volumes by theirs, and rejects a candidate whose UUID is not a version 4 UUID. Over the built image it reports the OS window volume 97A3EB1A-5218-4936-BC85-9574EC2C8CC9 and the SystemOS window volume 556915A0-5005-4DE5-B3E4-6B489DBB4C1A, and marks a third container embedded inside the OS volume rather than the window's own
Display
implemented
Components: GopFramebuffer.
Hardware and dependencies
- UEFI GOP framebuffer published by uefi-nx769j, its stride, pixel format and handoff.
- NX769J panel: 1116x2480 at 480 dpi (TARGET_SCREEN_DENSITY := 480).
- MDSS and DPU at qcom,sm8650-mdss / qcom,sm8650-dpu with two qcom,sm8650-dsi-ctrl and qcom,sm8650-dsi-phy-4nm instances.
- Backlight controls through the board panel driver.
Configuration
- GopFramebuffer is selected, but CONFIG_XNU_NATIVE_LEGACY_DISPLAY and CONFIG_XNU_NATIVE_MOBILE_DISPLAY are off: they reference native_legacy_display_probe and native_mobile_display_provider_probe from bsd_init and IOUserClient, and the late archive that defines them has no builder in this tree. The kernel links headless until the archive exists.
- Display presentation comes from the firmware framebuffer, so the board does not configure a scanout pipe yet.
Procedure
- Verify GOP address, dimensions, stride and pixel layout before enabling presentation.
- Build the late display archive with the selected OS ABI records, then check display publication and frame presentation.
- Record frame timing and pixel format at the checkpoints in https://unixnu.com/porting/bring-up.
Development findings
- The display source and ABI binding are selected independently of the physical panel configuration; the GOP path presents whatever geometry the firmware publishes.
- The panel geometry is measured on the device: card0-DSI-1 reports 1116x2480x60cmd. That matches the value recorded from the public NX769J board configurations, which also give 480 dpi.
- SM8650 carries two DSI controllers and a DSI PHY per controller; a native scanout path would bind to those rather than to the SM8550 display nodes.
Completion criteria
- A known frame appears with correct geometry and colors, then userland presentation and backlight controls work.
Current limits
- No presentation result exists yet; the firmware must publish a GOP framebuffer before the display area can be verified.
- Panel timing, backlight and DSI bring-up are unimplemented. GopFramebuffer presents through the firmware and does not drive the panel.
Next action
- Capture the GOP geometry from the firmware and record a known frame with correct geometry and colors.
Evidence
source: GopFramebuffer selection and the native legacy display flag described in this sectionsource: Measured panel geometry 1116x2480 from the DRM connector mode list
Input
planned
Hardware and dependencies
- NX769J touch controller is a Goodix 9916R. The device firmware ships goodix_firmware_9916r.bin and the running kernel registers a goodix_ts input device at goodix_ts/input0.
- The catalog input stack is ShengNovatekTouch, written for the Novatek NT36532E on sheng, with ShengHidInput publishing IOHIDDevice reports.
- Buttons register as gpio-keys_nubia with pmic_pwrkey and pmic_resin. Awinic SAR sensing registers as nubia_tgk_aw_sar0_ch0 and nubia_tgk_aw_sar1_ch0, and the fingerprint sensor is goodix_fp.
Configuration
- No input component is selected. board.toml components.include deliberately omits ShengHidInput, ShengNovatekTouch and QcomGeniSpi.
- Touch firmware paths and hashes are recorded in proprietary-files.txt for the future driver.
Procedure
- Read https://unixnu.com/porting/input and identify the touch bus, sensor protocol, pins, interrupts and firmware interface.
- Implement a Goodix 9916R driver against the catalog input interface, or adapt ShengNovatekTouch to the Goodix protocol.
- Map contacts to the display, confirm stable contact IDs across simultaneous touches and confirm release events.
Development findings
- The Novatek driver's sensor protocol does not match the Goodix 9916R on this board, so reusing it would require replacing the transport and the report parser rather than a configuration change.
- ShengHidInput publishes IOHIDDevice reports from a board touch event source, so the HID layer is reusable once a Goodix event source exists.
- The device confirms the touch controller family: the kernel registers goodix_ts and goodix_fp, and the SAR sensors are Awinic parts behind nubia_tgk_aw_sar* drivers. That matches the goodix_firmware_9916r.bin and aw9620x_* firmware already recorded in proprietary-files.txt.
Completion criteria
- Touches map to the display, simultaneous contacts keep stable IDs, release events arrive, and buttons produce expected HID reports.
Current limits
- No input driver exists for this board. Contact IDs, coordinate mapping and the interrupt wiring are not established, and the device exposes them only through the running Android input stack.
- Haptics and SAR sensing are out of scope for the first input milestone.
Next action
- Read the goodix_ts device node from the stock device tree to obtain the bus, address and interrupt, then implement a Goodix 9916R event source behind the catalog HID interface.
Evidence
GPU
planned
Hardware and dependencies
- Adreno 750, compatible qcom,adreno-43051401 at 0x03d00000/0x40000 with SPI 300.
- GMU qcom,adreno-gmu-750.1 at 0x03d6a000/0x35000 with rscc and gmu_pdc ranges, SPI 304 and 305 for hfi and gmu.
- GPU SMMU qcom,sm8650-smmu-500 at 0x03da0000/0x40000 with stream assignments 0 and 1 on the GPU and 5 on the GMU.
- ZAP shader firmware and GMU firmware for the Adreno 750 generation.
Configuration
- No GPU component is selected. board.toml components.include omits Adreno740, AdrenoMetal, Sm8550Platform, AdrenoPM4, AdrenoFirmware and ArmSmmu4K.
- CONFIG_XNU_NATIVE_ADRENO=n overrides the y set by soc:configs/sm8550.config.
- dts/nx769j.dts leaves &gpu, &gmu and &gpu_smmu disabled.
Procedure
- Read https://unixnu.com/porting/gpu and https://unixnu.com/porting/drivers.
- Qualify the catalog GPU stack against Adreno 750, or write a driver for this generation, then select the components and enable the DTS nodes together.
- Verify submissions, mapping and fence handling, render a known image and present it through the selected Metal client.
Development findings
- The catalog Adreno740 driver declares hardware Adreno 740 and SM8550 and is verified only on sheng. This board has Adreno 750, whose GMU reports qcom,adreno-gmu-750.1 where SM8550 reports qcom,adreno-gmu-740.1.
- GPU, GMU and GPU SMMU register ranges are identical between SM8550 and SM8650, so the SMMU and platform kits have matching register inputs; the firmware protocol and the PM4 packet generation are the parts that need qualification for this generation.
- sm8650.dtsi gives the GPU as qcom,adreno-43051401 while sm8550.dtsi gives qcom,adreno-43050a01; the chip ID is what distinguishes the two parts to the driver.
Completion criteria
- GPU submissions complete, mapping/fence handling is correct, a known image renders, and the selected Metal client presents it.
Current limits
- No GPU driver is qualified for Adreno 750 and no ZAP or GMU firmware is collected.
- GPU power votes, DCVS and the display scanout hand-off are unimplemented.
Next action
- Collect the Adreno 750 ZAP and GMU firmware from the device firmware package and qualify the HFI startup sequence against qcom,adreno-gmu-750.1.
Evidence
Collect evidence using the next action.
Network
planned
Hardware and dependencies
- WCN7850 (FastConnect 7800) reached over PCIe, with ath12k firmware and board data.
- The catalog Ath12kWifi transport declares hardware qcc2072, qcn9274 and wcn7850 build inputs and board dependencies on PCI configuration, BAR, MSI and DMA operations.
- Ath12kWifi declares os_abi 'nw_abi platform interface', which is not the ipados-17.7.11-21H461 ABI.
Configuration
- No network component is selected. board.toml components.include omits Ath12kWifi.
Procedure
- Read https://unixnu.com/porting/network and identify the bus, firmware, interrupts, DMA and OS interface.
- Qualify the ath12k transport against this OS profile's ABI, then select the component.
- Verify scanning, association, address configuration and sustained receive and transmit traffic.
Development findings
- Ath12kWifi is the only Wi-Fi transport in the catalog and its hardware list includes wcn7850, which is the chip on this board, but its declared os_abi does not match the selected OS profile. Selecting it would claim an ABI match that the catalog does not state.
- PCI configuration, BAR, MSI and DMA operations are board dependencies that this board has not implemented.
Completion criteria
- Scanning, association, address configuration and sustained receive/transmit traffic pass with the selected firmware.
Current limits
- No network driver is selected and no ath12k firmware is collected.
- PCIe enumeration and MSI delivery are unimplemented on this board.
Next action
- Establish the OS ABI that the ath12k transport requires, then either adapt it to ipados-17.7.11-21H461 or record the gap in the component catalog.
Evidence
Collect evidence using the next action.
Audio
planned
Hardware and dependencies
- SM8650 LPASS: qcom,sm8650-adsp-pas, qcom,gpr, qcom,q6apm, qcom,q6prm with lpass rx, tx, wsa and va macros over soundwire v2.0.0.
- ADSP firmware and its firmware services, loaded through the PAS loader.
- Board audio routes and the codec configuration shipped in the NX769J vendor firmware.
Configuration
- No audio component is selected. board.toml components.include omits the ADSP and audio IPC drivers.
Procedure
- Read https://unixnu.com/porting/power and the audio section of https://unixnu.com/porting/drivers.
- Bring up the ADSP with its firmware, establish the GPR/IPC router transport and publish the audio routes.
- Verify each declared route carries the selected audio format through playback or capture.
Development findings
- The ADSP is reached through the PAS loader and the GPR transport, so audio depends on firmware services and IPC routing before any codec work.
- The audio server keys its routing database and MediaExperience its per-board tables on the board name in the ADT, so the board name must be fixed before audio routes are qualified.
Completion criteria
- Each declared route carries the selected audio format through playback or capture and handles start/stop correctly.
Current limits
- ADSP firmware, IPC transport and codec routing are all unimplemented on this board.
Next action
- Record the ADSP firmware revision and the IPC transport entry points from the vendor firmware before implementing the transport.
Evidence
Collect evidence using the next action.
Power
planned
Hardware and dependencies
- PMIC regulators over SPMI with RPMh (qcom,rpmh-rsc, qcom,sm8650-rpmhpd, qcom,sm8650-rpmh-clk, qcom,bcm-voter).
- qcom,aoss-qmp and rpmh-stats for low power state transitions.
- NX769J battery and charging units, and the Awinic AW22xxx haptics driver.
Configuration
- No power component is selected. board.toml components.include omits the PMGR and power drivers.
- CONFIG_XNU_SHENG_PMGR is not set: the sheng PMGR driver is board specific and SM8650 has no Apple PMGR block.
Procedure
- Read the power section of https://unixnu.com/porting/drivers.
- Record regulator ownership, clock parents and the firmware ownership boundary, then implement the power manager and battery units.
- Verify declared power controls and battery readings against observations and exercise each claimed sleep and resume transition.
Development findings
- The SoC exposes RPMh and AOSS rather than an Apple PMGR block. The caihong SM8650 port reports that the AMFI early security init path panics with 'AMFI: No PMGR?' on this SoC family for that reason.
- SM8550 and SM8650 both expose the PMGR patch surface that UniXNU-sm8550-common patches, which is why the shared SoC source is usable here as well.
Completion criteria
- Declared power controls and battery readings match observations; each claimed sleep/resume transition restores its dependent peripherals.
Current limits
- Regulator ownership, battery units and sleep transitions are unimplemented. No power data is sourced for this board.
Next action
- Record the PMIC regulator inventory and the firmware ownership boundary from the vendor device tree.
Evidence
Collect evidence using the next action.
Image assembly
host-verified
Components: XnuPrelink.
Hardware and dependencies
- XNU 8796.101.5 at revision aca3beaa3dfbd42498b42c5e5ce20a938e6554e5, the open-source drop closest to Darwin 22.5.0.
- iOS 16.5 restore image for iPhone10,3, build 20F66. Several profile fields are read out of it and are still marked REQUIRED_IPSW.
- Android 14 boot image format: header version 4, 4096-byte pages, kernel base 0x0, LZ4 ramdisk.
Configuration
- board.os_profile = ios-16.5-20F66; the profile is os-profiles/ios-16.5-20F66/profile.toml and must land in UniXNU/profiles/os/ before a build resolves.
- kernel.prelink is inherited from the OS profile: tool kits/XnuPrelink/emit_prelink.py, slide -0x1000000, and kernelcache_sha256 and early_security_init are REQUIRED_IPSW.
- kernel.config_fragments apply arch:configs/common.config, soc:configs/sm8550.config and os:configs/ios.config, then the device defconfig.
- patch_series resolves arch, soc, os, device in order; the OS series is empty and the device series carries no board patch yet.
- [bootimg.image] base = 0x0, pagesize = 4096, header_version = 4.
- package/unixnu.toml defines the payload menu with a single xnu entry.
Procedure
- Read https://unixnu.com/xnu/source-and-patches, https://unixnu.com/concepts/prelink and https://unixnu.com/concepts/payload.
- Run python3 scripts/extract-ipsw-pins.py iPhone10,3,iPhone10,6_16.5_20F66_Restore.ipsw to reproduce every pin in the OS profile, including the ADT identity from the board device tree.
- Fill payload.text_fixups from the OS image dyld cache once native-text-fixup.patch is rebased onto xnu-8796.
- Rebase the arch, soc and XnuPrelink patch series onto xnu-8796 before referencing any of them from the OS series, then build kernel, package, firmware and payload in order.
- After changing boot arguments, rebuild both the payload and the boot image as described in https://unixnu.com/xnu/boot-arguments\#apply-arguments.
- Run python3 scripts/porting-record.py check --device . from the UniXNU checkout before publishing.
Development findings
- XNU 8796 is the iOS 16 kernel. The 8796 train has three open-source drops, 8796.101.5, 8796.121.2 and 8796.141.3; 8796.101.5 is the earliest and the closest to Darwin 22.5.0, so it is the baseline for iOS 16.5. The community iOS 16 ports, dipa-studio/ios-sweet and the ios-topaz family, build the same train.
- osfmk/conf/files.arm64 lists osfmk/arm64/kpc.c as optional kpc nos_arm_asm, but the file is not in the xnu-8796 source drop; only kern/kpc_common.c, kern/kpc_thread.c and x86_64/kpc_x86.c are published. Removing the entry is not enough: cpu.c calls kpc_idle and kpc_idle_exit and kpc_common.c calls the arch hooks, so the link then fails on eight _kpc_* symbols. xnu-8796-arm64-kpc-backend.patch provides the backend instead, reporting no counters and nothing running, which is what kpc_common.c asks before it reads anything.
- The iig interface generator is newer than this XNU. It emits rpc.kernelContent, reads it back as IORPCMessage * and initialises it with the kernel-resident message, while IORPC in xnu-8796 has only message, reply, sendSize and replySize. Adding IORPCMessage * kernelContent to the struct satisfies the generator; the 8796 dispatch path reads message only and ignores the field.
- The first kernel build reached the XNU compile and stopped on a single cause repeated four times: clang rejects -enable-trivial-auto-var-init-zero-knowing-it-will-be-removed-from-clang, which makedefs/MakeInc.def adds beside -ftrivial-auto-var-init=zero. That companion flag was removed from clang. linux-clang-auto-var-init.patch drops it and keeps -ftrivial-auto-var-init=zero, which is the supported way to get the same zero-initialisation. The patch is last in the series so it cannot collide with the other MakeInc.def edits.
- The second build stopped on uio_stackbuf_t, which bsd/sys/uio_internal.h declares as char aligned to union iovecs and then places in arrays sized by UIO_SIZEOF. Upstream clang rejects an array whose element size is not a multiple of its alignment; Apple's clang accepts the idiom. linux-clang-uio-stackbuf-align.patch moves the alignment to the array declarations and leaves the typedef as plain char, so sizeof stays 1 and UIO_SIZEOF still counts bytes. Verified on Ubuntu clang 18.1.3: the element is 1 byte, the buffer keeps its byte count, and the array is 8 byte aligned.
- zzz-boot-progress.patch inserted its boot_progress_user_abort call between two lines of handle_kernel_abort's parameter list, which is a syntax error wherever that signature appears. Verifying a fuzzy application by comparing the added and removed lines caught nothing, because the change set was identical and only its placement was wrong. Comparing what changed is not enough; a re-anchored hunk needs its placement checked too. The call now goes at the top of the body, matching the other boot_progress hooks.
- KALLOC_TYPE_IS_COMPATIBLE_PTR takes the pointee type with __typeof__(*(ptr)). In C++ that is ill formed for a void pointer, which the macro explicitly documents as a wildcard, so any C++ translation unit that frees a void pointer fails. __builtin_choose_expr does not help: clang still diagnoses the unselected branch. linux-clang-kalloc-pointee-type.patch derives the pointee type from the pointer type with a template in C++ and keeps the deref in C.
- The kernelcache pin covers the decompressed arm64 slice, not the whole fat image. scripts/unixnu/ipsw.py takes the IMG4 payload, LZFSE-decodes it, then keeps the single 0x0100000c fat slice and hashes that; hashing the fat image gives a different digest and fails the board pin check. scripts/extract-ipsw-pins.py mirrors that pipeline exactly.
- The prelink ABI is version bound and was read out of the 16.5 kernelcache rather than copied: AppleMobileFileIntegrity sits at 0xfffffff004e2b640 here against 0xfffffff005ab4ae0 in the 17.7.11 profile, and com.apple.iokit.IOHIDEventDriver, which the 17.7.11 roots list keeps, does not exist in this kernelcache at all.
- The ADT identity comes from the real board device tree, Firmware/all_flash/DeviceTree.d22ap.im4p inside the restore image, not from libMobileGestalt and not from another profile. That tree gives product-id fd079b298144a7deb4d7c87fef3eb474d8f0e7eb and corrects three values that would otherwise be inherited from the iPadOS profile: artwork-device-subtype is 2436 rather than 2732, compatible-device-fallback is iPhone9,2 rather than iPhone10,6, and kern.maxvnodes is 10000 rather than 66560.
- The Apple device tree length field carries a flag in its high bit; the real length is the low 31 bits. Reading it unmasked overflows the parser.
- The rebase is complete and verified: all 59 patches apply strictly, in order, onto xnu-8796.101.5. 54 carried over unchanged, one needed the hunk re-anchored for a build-config file whose surrounding comments drifted, and two hunks were dropped because the include line they rewrite does not exist in this train.
- Patch order matters and patches depend on earlier ones creating their target code. zone_id_require_aligned is introduced by offline-pgz-alignment-probe and FREEZE_ENABLED_DEFAULT by another patch in the series, so testing a patch in isolation against a clean tree gives the wrong answer. The series was regenerated by replaying it in order.
- pgz-aligned-validation and published-freezer-condition are dropped entirely. Both patch code that no patch in this train creates and that xnu-8796 does not have: zone_id_require_aligned is only referenced inside the XNU_ZONE_PGZ_ALIGNMENT_PROBE block, which is off by default and has no definition in 8796, and the FREEZE_ENABLED_DEFAULT block does not exist here.
- zzx-pmap-sync-64bit-pa patch halves split by target: osfmk/arm64/sptm/pmap/pmap.c does not exist in 8796, so that half is dropped, while its osfmk/arm/pmap/pmap.c half applies and is kept.
- The two prelink patches, security-init and sparse-index, apply to xnu-8796 without modification. XnuPrelink only ships them under xnu-10063 file names, so the copies here are renamed to xnu-8796.101.5-* to name the train they run on.
- board.toml selects only device:patches/xnu/series. The shared arch, soc and os series still target xnu-10063.141.1; referencing them would abort unixnu status with 'patch missing' or fail during application. Switch back to the four-series form once those are rebased.
- native-text-fixup.patch is among the rebased set, so payload.text_fixups is the remaining input for that path rather than a separate track.
- The boot image header version is 0, not the 4 that the NX769J board configuration records. BOARD_BOOTIMG_HEADER_VERSION is the Android build system setting for a stock boot.img; unixnu bootimg verify() unpacks ten u32 words from offset 8 and reads header_version and pagesize at the v0/v1/v2 offsets, so a v4 header fails the round-trip check. The tool defaults use 0. The sibling RedMagic port UniXNU-nx729j hit the same failure, fixed it the same way, and then fastboot-booted the image on hardware. If the NX769J ABL rejects v0 the fix is to extend verify() rather than to set the value here.
- The caihong SM8650 port omits prelink early_security_init because running AMFI early security init on a SoC without an Apple PMGR panics with 'AMFI: No PMGR?'. NX769J is the same SoC family, so treat the hook as an open risk and record the observed behaviour rather than assuming it runs.
- The image assembly now runs end to end. build userdata writes userdata-root.img 8357150720 bytes with the OS window from 096-04088-077.dmg at 0x100000 and the SystemOS window from 096-04666-078.dmg at 0x14fa00000; build payload writes payload.img 218103808 bytes carrying the OS trust cache; bootimg packs boot.img 220332032 bytes and round-trips it. The order matters: build userdata before build payload, because scripts/extract-root-volume-uuid.py reads the root volume UUID out of the userdata image. On Linux the image steps need the toolchain bin directory on PATH, since build userdata decodes LZFSE chunks with the lzfse tool that scripts/linux/setup-toolchain.py installs.
Completion criteria
- Kernel and prelink succeed, payload contains all declared inputs, and the boot image matches the bootloader format and size limit.
Current limits
- The kernel compiles and links on xnu-8796 with this series: the build reaches 'built kernel at build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837/mach.development.bcm2837'. Nine of the ten archive builders now pass - corecrypto twice, ArmDTPlatform, ArmCpu, SoftwareAES, the SoftwareAES bindings, the libpthread kernel component, ShengUfs and VirtioMmioBlock - and the missing per-object compile records are supplied by scripts/compile-records.py through the $(CCACHE) $(CC) hook that MakeInc.def leaves undefined. The Build workflow sets CCACHE so the same records appear on CI. XNU C rules compile without -o and let clang derive the object name, so the record states the output explicitly when the invocation leaves it implicit: build-mockfs-fixture.py and build-libpthread-kernel.py both substitute -o and otherwise write the object somewhere the consumer does not look. build-mockfs-fixture.py is also dropped from [kernel] archives, because the kernel only force-loads that archive when MOCKFS_MODE is 1 and this board leaves CONFIG_XNU_SOURCE_MOCKFS unset. The pinned libpthread is the iOS 17 drop and is one API ahead of xnu-8796 in exactly one place, proc_get_jit_entitled; patches/libpthread/ adapts it and carries UniXNU's explicit-kernel-address.patch unchanged, since a device patches/libpthread directory shadows the upstream one. The kernel now builds and links: all eleven archives pass and mach.development.bcm2837 is produced with no undefined symbols. The three gaps the link first reported were closed without a device. The eleven IOBlockStorageDevice symbols come from scripts/build-storage-bindings.py, which binds the inherited slots to the original prelinked kext code with absolute symbols taken from scripts/prelink-storage-bindings.json -- measured statically out of the iOS 16.5 kernelcache by scripts/scan-prelink-storage.py, whose output records kernelcache_sha256 e6fa7559... and so proves it read the pinned image. _boot_progress_poll is the weak observer NativeUfs calls only while verifying, and is defined as a no-op. The six verified-root symbols come from scripts/build-verified-root.py, which compiles the real VerifiedRoot crypto rather than stubbing it and generates an empty policy table: verified_root_policy_count is 0, which makes configureVerification return before it reads the key or calls vr_open, so root verification is disabled rather than approximated. A signed policy bundle is the one input still genuinely missing, and it is policy data, not code. Prelink gets through plan() and through the instruction inventory: the one literal pool that stopped it is known to scripts/emit-prelink-nx769j.py, which re-derives its boundary from the image and refuses to run if the pool no longer sits between a function epilogue and the next prologue. What remains is the early security initializer address -- see next_action. The inventory was a safety check rather than a transform, and loosening it would let real code be classified as data and drop a genuine external encoding silently. emit_prelink.py reports instruction_relocation_complete, abi_qualified and loadable as false in every plan it writes, so the artifact it produces is an experimental candidate and not a qualified kernel however these questions are settled.
- payload.text_fixups is an empty list, and that is the honest state rather than an approval. Entries disable code in a signed file after signature validation -- the worked example ends in c0035fd6, which is `ret` -- and each is a specific byte change in the dyld shared cache inside 096-04088-077.dmg, chosen to bypass whatever turns out to be in the way. None has been determined for this port. They are not derivable mechanically, and an empty list means none needed yet rather than none will be.
- The four inputs that only a device held are all collected now. The 52 vendor UEFI DXE drivers and the firmware DTB come out of the device flash package with scripts/extract-uefi-drivers.py and scripts/extract-firmware-dtb.py, and both are pinned by hash in proprietary-files.txt; the unique chip id is recorded in device-inputs.md and passed as --unique-chip-id; the root volume UUID comes out of the built userdata image with scripts/extract-root-volume-uuid.py. What assembling the images still needs is a local restore image and a local trust cache, neither of which is redistributable. The payload and the boot image now build: payload.img 218103808 bytes carrying the OS trust cache df0f60fd..., and boot.img 220344320 bytes with the FD and DTB that boot.json records. The payload and boot image are not byte-reproducible: the ADT carries a 64-byte random seed and the FD it packs is not either -- see docs/HOST-VERIFY.md.
- The Data seed is in the payload: gh run download fetches the nx769j-data-seed artifact and build payload takes it as --data-ramdisk. Each run formats a new volume, so the Data volume UUID changes with it and nothing pins one. The kernel refuses a seed whose store window does not fit the partition, so the container size has to follow the userdata geometry rather than be chosen freely.
- The rebased copies are duplicated here rather than in the owning ARM64, SoC and component repositories, which this repository does not control. They should move back once those series are rebased.
- The restore image is not redistributable, so ipsw.url carries the member name and the build takes a local copy through --ipsw or UNIXNU_IPSW.
Next action
- Measure the AppleMobileFileIntegrity early security initializer and set kernel.prelink.early_security_init to it. The contract is in patches/xnu/xnu-8796.101.5-security-init.patch: OSKext::start reads an _NativePrelinkEarlySecurityInit OSNumber from the kext, adds vm_kernel_slide, checks the result is 4-aligned and inside a read-execute non-writable segment of that module, and calls it as void(void) before MAC policy lockdown. So the value is an unslid function address inside the module's __TEXT_EXEC, which for this kernelcache is 0xfffffff0056fe000..0xfffffff00571b764. Identifying which function is a reversing task on a stripped kext, not a field lookup -- the __PRELINK_INFO dictionary only names the kmod stub. Until it is measured the field stays unset and emit_prelink skips the path; AMFI still starts through its ordinary start routine, but the pre-lockdown call does not happen. Separately, produce the owner-signed verified-root policy bundle over the root windows of the OS image that gets flashed and replace the generated empty table in scripts/build-verified-root.py with it, so root verification is enabled rather than disabled.
- Set the Data volume UUID the seed run reports in whichever configuration the block driver reads it from, and set the ARTIFACT_PASSPHRASE secret so the seed is uploaded encrypted rather than holding restore-image files in the clear.
- Derive a payload.text_fixup when a boot gets far enough to fail on one; there is nothing to derive before that.
- Upstream the rebased series to the ARM64, SoC and XnuPrelink repositories and restore the four-series patch_series form.
Evidence
source: Payload menu and boot entry definition for the assembled imagesource: iOS 16.5 XNU pins, Darwin version, IPSW identity, prelink ABI inputs and the ADT identity, with each field sourced in a commentsource: The complete rebased patch order, verified to apply strictly in sequence onto xnu-8796.101.5source: Per-patch applicability against xnu-8796.101.5, the dropped hunks and their reasons, and the reproduce commandssource: Reproducible extraction of chip_id, board_id, member hashes, kernelcache_sha256 and the kext inventory and the ADT identity from the restore imagesource: Stops libpthread advertising PTHREAD_FEATURE_JIT_ENTITLED, whose proc_get_jit_entitled callback xnu-8796 does not have; the other 59 of the 60 callbacks the pinned libpthread uses resolve against that kernelsource: Why the device carries a patches/libpthread directory at all: module resolution is first-wins and it shadows UniXNU's, so explicit-kernel-address.patch is carried byte for byte rather than lostsource: Measures the original IOBlockStorageDevice dispatch table out of the iOS 16.5 kernelcache: the vtable and its 190 slots, the metaclass, the protected constructor and the base destructor. No hardware, no live snapshot - every value is read from the imagesource: The measured pin, recording kernelcache_sha256 e6fa75598f47b5a9460a60913e2ac8e4cd6cd60eb475bb31da65b68ffbcfdad5, which matches the kernelcache_sha256 this repository already pins in the OS profilesource: The measured board inputs and, now, a cross-check of the splash reservation against the public firmware dump's DTBO overlay -- which agrees exactly and adds disp_rdump_region at 0xe3940000 for 0x800000source: payload.text_fixups is an empty list rather than a placeholder: the entries disable code in a signed file and are discovered per problem, so there is nothing to derive before a boot reaches onesource: Runs emit_prelink.py with this kernelcache's com.apple.kec.Libm coefficient table known, re-deriving the pool boundary from the image and refusing to run unless it still sits between a function epilogue and the next prologuesource: Builds the real VerifiedRoot crypto together with a generated policy table that truthfully reports no signed bundle, so the UFS driver runs with root verification disabled rather than approximatedsource: The prelink input is mach.development.bcm2837.unstripped, not the .unstripped.noctf that xnu-10063 names: xnu-8796 links no such output and its raw kernel carries no CTF to strip. early_security_init is left unset because the only address scripts/extract-ipsw-pins.py could derive, the kmod stub, is not the function the hook callssource: Records each XNU compile as <object>.json and now states the output explicitly when the invocation leaves it implicit, which is what build-mockfs-fixture.py and build-libpthread-kernel.py need in order to substitute -osource: Builds the writable Data seed for --data-ramdisk from the iOS 16.5 OS volume on macOS: an APFS OwnerData volume at the size asked for, holding the volume's initial /private/var without staged_system_apps, packed with DarwinBoot's DBRDISK1 writer so only the non-zero pages travel. UniXNU's own prepare-owner-data-fixture.py and seed-data-grow.py cannot do it: the first rejects any OS mount whose sbin/launchd does not hash to the iPadOS 17.7.11 image and the second grows a packed seed this port does not have. .github/workflows/data-seed.yml runs it on a hosted macOS runner and reports the new volume UUID. Several runs failed before it produced anything, and job logs need repository access, so each was diagnosed from the run page annotations instead: a cache key holding a comma, tee masking the exit code, a 32 MiB container grown rather than created at size, a missing DarwinBoot submodule, Path.symlink where the method is symlink_to, and a log redirect into a directory that did not exist yet. scripts/test_build_data_seed.py covers the copy, the tree measurement, the mount checks and the container check, which is everything that runs without hdiutilhost: Run 41026291337 on macos-26 built nx769j-data-seed, 567485 bytes, sha256 589fb90de70a9d5b5e367464a66674c659c7bd32ee93ced445ead9f2f77136bd, holding a 256 MiB APFS OwnerData container with the iOS 16.5 OS volume initial /private/var and Data volume UUID DA9A01BC-22A9-47B0-8EA0-27B3C35E37E4. It is uploaded unencrypted because ARTIFACT_PASSPHRASE is not set, and downloading an artifact needs a token even for a public repository. gh run download fetched it as nx769j-data.dbr, 970848 bytes, sha256 e13f93e046ad2d60e8d3be706577be2d207df0c303b52682a2b54b6ad18da2bc holding 36 files and 589907 bytes of the OS volume initial /private/var, and build payload --data-ramdisk put it in the payload as scratch-data.img behind initrd = Payloads/scratch-data.img; the seeded payload.img is 8f5d75ff... and boot.img 834403f3... at 220344320 bytes. gh repo edit --add-topic unixnu published the repository to the device list Command:./unixnu build payload -D ../UniXNU-NX769J --data-ramdisk nx769j-data.dbr --unique-chip-id 8abc5430 --uefi ../uefi-nx769j --ipsw <restore.ipsw> --trust-cache <096-04088-077.dmg.trustcache>. Revision:ed45ee86452bc50735625fcf124fa2911d364ef9.host: 2026-10-30: all three image steps ran in order. userdata-root.img 8357150720 bytes sha256 818e4e37bc9061ac8f5a2103234adb1c1b945fa1f2f4255ac148731b9ebf2631 holds the OS window at 0x100000 and the SystemOS window at 0x14fa00000; payload.img 218103808 bytes sha256 a01c88e13681ff83ba3059c9bf0da00a4cdcceac041af24e28898d753ae1e6f9 carries kernel.macho 2458882f... and the OS trust cache df0f60fd..., and its menu lists xnu, reset, shutdown and tools; boot.img 220344320 bytes sha256 1b1f22c2513e16ec50463345daa45b2cf8244881d0841b174cdc4b1d6ea38a4a packs FD b0690638... and DTB f336a7d7... under an ANDROID! v0 header at 4096-byte pages. bootimg round-trips the image and failed no check. The kernel itself stays an experimental candidate: emit_prelink.py still records instruction_relocation_complete, abi_qualified, loadable and runtime_qualified as false. Command:./unixnu build userdata -D ../UniXNU-NX769J --ipsw <restore.ipsw> && ./unixnu build payload -D ../UniXNU-NX769J --unique-chip-id 8abc5430 --uefi ../uefi-nx769j --ipsw <restore.ipsw> --trust-cache <096-04088-077.dmg.trustcache> && ./unixnu bootimg -D ../UniXNU-NX769J --uefi ../uefi-nx769j --dtb ../uefi-nx769j/Resources/DTBs/nx769j.dtb -o out/nx769j/ios-16.5-20F66/nx769j/boot/boot.img. Revision:dd97bc4b307f77df2f6189b7c92881133cc24ac3.
Diagnostics
planned
Hardware and dependencies
- Firmware log exporter and QR log export from https://unixnu.com/porting/log-capture\#firmware-setup.
- Serial console over kernel argument serial=3.
- The NX769J device firmware revisions: baseband 5.0.c3-00030-LANAI_GEN_PACK-1.46586.20 and trustzone TZ.XF.5.0-07512.2-LANAIAAAAANAZT-1.46586.1.
Configuration
- kernel_args are '-v serial=3'; payload.panic_screen = true and payload.timeout = 3.
- scripts/collect-device-inputs.sh gathers the board inputs that no public source provides.
Procedure
- Read https://unixnu.com/porting/log-capture and https://unixnu.com/porting/bring-up.
- Set up QR log export during firmware integration and save the firmware log before the kernel handoff.
- Record each command, revision, output and result, and assign each area its completion level from https://unixnu.com/porting/records\#completion-levels.
Development findings
- Each claimed milestone needs a reproducible command, a matching source and image revision, and an accessible result. Store large logs as linked release or CI artifacts.
- The firmware revisions identify a verification result on this device, so they belong in every device evidence entry once one exists.
- device-inputs.md records the board inputs measured over read-only adb, with the command behind each value. The handset reports a spoofed Xiaomi identity through its userspace properties while its boot image, kernel and device tree identify NX769J, so identity and hardware values were read from getprop of the bootimage properties, the kernel, the device tree and the raw UFS partition table rather than from the spoofed product properties.
- scripts/collect-device-inputs.sh is the same collection procedure in runnable form and remains the way to re-measure before writing to a device.
- The merged kernel configuration can carry either the CONFIG_XNU_ name from the defconfig syntax or the bare XNU_ name the kernel build consumes, and a false value can appear as n, 0 or '# NAME is not set'. scripts/check-merged-config.py accepts all of those forms so the check reports a real value mismatch instead of a format difference.
Completion criteria
- Each claimed milestone has a reproducible command, a matching source/image revision and an accessible result.
Current limits
- No port verification exists on the device yet: the measurements are hardware facts, not evidence that the port runs. Configuration, firmware and packaging carry host evidence with the command and commit behind it; the rest stay source evidence. Nothing carries device evidence until an image boots.
- Log capture and recovery commands are not set up. The firmware is built and the serial console is wired through kernel_args serial=3, but nothing has been started on hardware to capture.
Next action
- Set up QR log export in the uefi-nx769j firmware and capture the firmware log before the first kernel handoff.
- Re-run the device-inputs.md commands immediately before any write, since the recorded GPT extents describe one unit at one point in time.
Evidence
source: Measured identity, partition geometry, reserved memory, DRAM, register map, display and input facts with the read-only commands that produced themsource: Preflight for the uefi-nx769j platform package, jointly owned with the firmware area because it matches the scripts/check-* pattern as well as the firmware pattern: it checks the Mu-Silicium layout, board.toml agreement, the SM8650 SoC package include and the vendor DXE inventory. The vendor set is read from the platform's three includes rather than held in step with them, and both checkouts are found from the assembled tree the preflight runs insource: A gate on the UEFI memory map, jointly owned with the memory area because it matches scripts/check-* as well as scripts/*memory*. A wrong map lets the firmware write over what the bootloader or TrustZone owns and the failure is a corrupted boot rather than a build error, so the table is compared with the device tree instead of being reviewed