An (almost) fully libre Galaxy S3

It's approaching three months since I started writing this blog post. In the mean time, life has gotten in the way of my work on U-Boot and Linux. One cool thing I've been working on is the Advanced Operating Systems course at UNSW - writing an operating system from (almost) scratch, based on the seL4 microkernel. That is probably worthy of its own blog post at a later point, though...

Since my last post on U-Boot, much has been achieved: U-Boot is now a usable (albeit slightly barebones) bootloader on the Galaxy S3. Now it's able to manage important stuff like battery state, charging status, and eMMC... and booting Linux successfully. But - as they say - it's not just about the destination - the journey is important, too!

Once U-Boot was able to reliably boot into a console (as at the end of our first post), the next step to being able to develop easily was being able to boot U-Boot off the eMMC storage - since booting off the SD card, while doable, involves removing the mainboard from the phone, holding the battery in place with one hand, and holding a pair of tweezers in place with the other. This is... awkward. Fortunately, eMMC support for Exynos 4412 is already present in U-Boot, so it's a just a matter of setting a flag, crossing fingers, and flashing the assembled U-Boot binary to /dev/block/mmcblk2boot0 in Linux. Great!

Batteries

On mobile phones, the battery is quite important: If the battery's charge is too low, the bootloader should refuse to boot. If the phone is plugged in, the phone gets turned on - but a lot of the time, it's more desirable that the phone just enters a "low-power mode" that indicates charge level but doesn't do much else. Unfortunately, U-Boot's support for batteries and chargers hasn't been significantly updated in four years. Since then, U-Boot has obtained a "driver model", allowing for drivers to be probed dynamically based on a DTS rather than statically based on board files. So, first: a new API needs to be defined for batteries, allowing access to information about voltage, battery status ("not present", "needs charging", "ok"), and "state of charge" (percentage).

Another API is needed to manage chargers: It supports operations like "set maximum charging current", "get charge current" and "get charge state" ("charging", "full", "discharging", "not charging", "discharging"). Next, comes adding a charger and battery driver for the i9300.

Now that we don't have to worry about the phone becoming unbootable because of a critically low battery, it's time to try actually booting a kernel. First, though, we need a way to get a kernel on the device.

USB

Fortunately, U-Boot already has support for the DesignWare USB device-mode controller found in the Exynos 4412. All that's needed is to add some setup code, and enabling fastboot means we can run fastboot 0 in U-Boot, plug in the USB port, and see...

 $ fastboot devices
????????????        fastboot

To change the question marks to a sane value (i.e. the serial number), I had to figure out where the original bootloader gets the serial number from. After much time spent trawling through the eMMC with xxd, eventually I figured out where the serial number comes from: it's the bottom 64 bits of the 128-bit eMMC serial number. All we need to do is load this info when booting up, and there we have it:

$ fastboot devices
4df7658224facfb9        fastboot

Now what we can do is use fastboot to boot a kernel without even flashing it to the device: fastboot boot boot.img, for instance. But of course, this doesn't work straight away - that would be far too easy! The kernel starts, but hangs very early in the boot process. What's changed?

Firmware

Most (if not all) recent ARM SoCs have two worlds: There's the secure world, which does "secure" stuff: like DRM, storing encryption keys, etc. Then there's the non-secure world, where normal code (like Linux) runs. On a normal i9300, the second bootloader stage loads a special firmware (called MobiCore) into the secure world, and sets up the SoC so that some RAM is set aside for it, "secure" peripherals (like the DRAM controller) are only accessible by it. Once MobiCore has started, the second bootloader loads S-BOOT into the non-secure world, and normal boot continues. The secure world takes control over things like hotplugging CPUs, in order to stop privilege escalation by hotplugging in a CPU (which comes online in the secure world) and running your own code. Instead, the secure firmware drops the CPU out of the secure world and into the non-secure world before start the Linux kernel's CPU startup code.

The Linux kernel tells the secure world that it wants to hotplug in/out a CPU using a special ARM instruction, secure monitor call (or smc). This is just like a normal system call, but instead of going from user mode to kernel mode, it goes from non-secure mode to secure mode. This is where the problem lies: the Linux kernel for i9300 expects a secure world - but under U-Boot, there is no secure world to use, and so the whole system crashes when trying to hotplug in a CPU. The fix is simple: just remove the "secure-firmware" node from the DTS. We can even do it automatically, when using something like a FIT image rather than an appended DTB.

Now, finally, it boots. Right? Well, yes.

Welcome to Buildroot
buildroot login: root
Password: 
# ls
bin      etc      lib32   [   16.366260] dma-pl330 12680000.pdma: Reset Channel-3        CS-20000f FTC-20000
   sbin     usr
# 

DMA

Hmph. That's not normal. A little bit of casual datasheet reading shows that this error is a "data_write_err" - the DMA ("Direct Memory Access" - used to allow peripherals to access memory without requiring CPU involvement) controller is accessing an illegal address when attempting to transmit some data over UART. One useful piece of information that I only found out about later is that the serial port driver will not use DMA when small amounts of data are being transmitted/received. So, it seems likely that the DMA controller straight up doesn't work. It does work on a separate i9300 running secure firmware, so something must be different between the two. Given that the source of the last problem was the secure world, that seems like a reasonable place to start.

When worlds collide

To enforce the restrictions on non-secure world peripheral and address access, the Exynos 4412 comes with a "TrustZone Protection Controller", and a "TrustZone Address Space Controller". These control whether any given peripheral or memory address is accessible to the secure world only, or both the secure and non-secure worlds. Another important thing to note is that even though the CPU is running in secure mode, the PL330 DMA controller always uses non-secure mode. So, if you try and use DMA to access something that's set in the TZPC as "secure only", you'll get a fault, and the DMA transaction will fail.

All that's needed is to mark all memory as non-secure read/writable, and mark all peripherals as non-secure read/writable.

Almost?

And there we have it: we've successfully removed almost all blobs from the Exynos 4412 found in the i9300. The one blob remaining is probably not replaceable: it's both encrypted and signed by Samsung. However, since we have access to where the code is stored, it would be trivial to wipe every trace of it from RAM once U-Boot starts. (And similarly, it's easy to dump the code that's been loaded into RAM - it is not even remotely interesting...)

One bootloader to rule them all

Now there's one thing left to do: Add support for the rest of the "midas" family - that is, i9305, n7100, and n7105. The tricky bit here is that it's very hard to tell the boards apart from the bootloader. On a high level, it seems simple: i9305 and n7105 have LTE modems, and n7100 and n7105 are bigger. Unfortunately, neither of these pieces of information are readily available in the very early stages of the bootloader. Fortunately, though, there is a (surprisingly easy) way to tell the difference. The bus used to communicate with most peripherals found on these boards is I2C. An I2C bus uses a "pull-up" resistor on each of the data lines, which means that even if we set the bus to use the SoC's internal pull-down resistor it will stay high. Careful examination of the vendor source files shows that each of these boards have different I2C bus locations: for instance, N7105 and I9305 have an I2C bus where I9300 and N7100 have GPS UART flow control pins. The result of this? N7105 and I9305 leave these two pins "high" when pulled low, but i9300 and n7100 don't. By applying this strategy across multiple buses, we can determine which device is which.

Try it yourself

If you're after something to tinker with, have a spare GT-I9300/GT-I9305/GT-N7100/GT-N7105 lying around, and don't really care about using anything based on the old kernel (be that LineageOS, Replicant, or the stock firmware), you can even try this for yourself. While it should be possible to go back to the proprietary bootloader, I have not tested doing so: doing this would be left largely as an exercise to the reader. It's also worth mentioning that LineageOS/Replicant will boot under u-boot with a kernel compiled without CONFIG_ARM_TRUSTZONE set, but the display will not work (probably because u-boot does not initialise the display).

The mechanism this uses should also be portable to any other Exynos 4412-based board.

Note: You should BACK UP anything that you care about on your phone, ESPECIALLY the EFS partition (which is irreplaceable!). On everything except i9300, also back up m9kefs1, m9kefs2, and m9kefs3. While this process shouldn't do anything bad: better safe than sorry!

If you brick your phone somehow, you will be able to recover it by preparing an SD card and shorting a resistor on the phone's mainboard. The resistor can be tricky to short properly, but it's generally doable. I will write instructions on this procedure at some point (feel free to email/ask me on IRC if you end up bricked before I do this...).

  1. Download and flash a suitable TWRP image to the RECOVERY partition on your device. This is simply official TWRP, with a tiny patch applied on top of the kernel to show the partition that contains the bootloader.
  2. Boot into recovery the normal way (Power + VolUp + Home)
  3. Backup the original bootloader: dd if=/dev/block/mmcblk0boot0 of=/sboot.bin, and pull it off the device. (If you want to go back, you should just need to dd this file to /dev/mmcblk2boot0 from the mainline kernel).
  4. Backup the EFS partition: dd if=/dev/block/platform/dw_mmc/by-name/EFS of=/efs.img , and pull it off the device.
  5. Download and push the prebuilt U-Boot image to the device. Verify the sha256sum. You can use this image on any of GT-I9300, GT-I9305, GT-N7100, or GT-N7105.
    • Alternatively, you can build the bootloader and kernel using my buildroot tree. The bootloader will be in output/images/bootloader.bin, and the kernel will be in output/images/boot.itb.
  6. Enable writing to the MMC boot partition: echo 0 > /sys/block/mmcblk0boot0/force_ro.
  7. Write the bootloader: dd if=/u-boot-2018-06-19.bin of=/dev/block/mmcblk0boot0.
  8. Verify the bootloader was written OK: hexdump -C /dev/block/mmcblk0boot0 | head -n 1 should output: 00000000 a3 69 d3 18 ec ad 6f b0 07 bd 03 b8 02 af 70 af |.i....o.......p.|.
  9. If the output doesn't match, repeat steps 7-8 until it does.
  10. Run reboot download. The device will power off, and within about a second a blue LED should come on.
  11. Plug in the device to a computer, and run fastboot devices. You should see a single device - this is your phone!
  12. Run fastboot getvar bootloader-version. You should get back something like bootloader-version: U-Boot 2018.03-midas.
  13. Download the prebuilt Buildroot image, and boot it using fastboot boot buildroot-2018-09-09.itb.
    • It is worth noting that by default, this image includes firmware necessary to use wifi. The driver will not automatically load, though - you can probe the driver using modprobe brcmfmac, and then configure wpa-supplicant appropriately (or, set up a buildroot rootfs overlay including your wifi network information and ssh public key).
  14. Wait a few seconds, then unplug/replug the USB cable (or wait ~25 seconds and don't unplug/replug the USB cable).
  15. screen /dev/ttyACM0 115200, or picocom -b 115200 /dev/ttyACM0 should get you a prompt something like this...

    Welcome to Buildroot buildroot login: - You can log in using username root, password root.

LED

The notification LED will light up to indicate what the bootloader is doing (in absence of a screen).

  • Red: recovery mode (will not work if recovery image not flashed)
  • Red, flashing: the device is charging before it proceeds with another boot mode
  • Blue: fastboot mode
  • Green: U-Boot console mode
  • Yellow: normal boot
  • White, flashing: the device needs to be charged before it can be booted, but isn't plugged into a charger.

Key combinations

The keycombos are the same as the original bootloader, with one extra.

  • Power + Volume Up + Home: Recovery mode (will probably not do anything unless you're using the right partition table and have something flashed there)
  • Power + Volume Down + Home: Fastboot mode
  • Power + Volume Up + Volume Down: Drops to a U-Boot serial console. You'll need a serial adapter to access this.

Register values

You can write magic values to the PMU register INFORM3 (located at 0x1002080c) to boot into these special modes without physical interaction (using a tool like devmem - e.g. devmem 0x1002080c 0x12345671 && reboot):

  • 0x12345670 - normal reboot (default)
  • 0x12345671 - fastboot mode
  • 0x12345672 - recovery mode
  • 0x12345673 - u-boot console
  • Any other value - normal boot

This register is only cleared on a hard reset (i.e. holding down the power button or pulling the battery).