Building a boot image for Razer Forge TV

The Razer Forge TV was an Android-TV based micro console and the sole reason that Razer bought OUYA - to make the games library available on the Forge TV. It was discontinued in 2016, the server was shut down in 2019.

I'm now trying to resurrect its "Corex" store just as I did with the OUYA and the MadCatz Mojo.

When starting a factory reset Forge TV with firmware M-144, the Android TV setup wizard appears and requires you to sign in with a Google Account. Entering an e-mail address and password does not work anymore in 2023, probably because Google removed the necessary APIs.

Enter your Google Account email address Couldn't sign in

People say that it is possible to sign in with a second Android device that is near the Forge. I don't have an Android phone with the "Google" app required for this and am stuck.

Do you have an Android phone or tablet? Connecting to Google... Set up your TV with Android

A way around that setup wizard would be to mark the setup as completed, so that it does not start at all. This requires debugging access via adb, which I don't have.

So I need to modify the boot image to enable adb.

Unpacking the boot image

Razer published two firmware versions, of which I chose the newer one:

M-144
The latest, and based on Android 6.0.1
RM-152
Predates M-144 and is based on Android 5.1.1. It is a stock Android TV without the Cortex store.

The firmware image is a .zip file with some partitions inside:

$ unzip -l ../image-forge-m-144.zip
Archive:  ../image-forge-m-144.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       22  2016-11-09 10:36   android-info.txt
 14630912  2016-11-09 10:37   boot.img
 15351808  2016-11-09 10:37   recovery.img
978758664  2016-11-09 10:38   system.img
---------                     -------
1008741406                     4 files

boot.img seemed to be a standard boot image:

$ file boot.img
boot.img: Android bootimg, kernel (0x8000), ramdisk (0x1000000), page size: 4096, cmdline (console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom user_debug=31 msm_rtb.filter=0x3b7 dwc3_msm.cpu)

I used abootimg (There is a Debian package!) to extract the contents:

$ abootimg -x ../boot.img
writing boot image config in bootimg.cfg
extracting kernel in zImage
extracting ramdisk in initrd.img
 
$ file initrd.img
initrd.img: gzip compressed data, from Unix, original size modulo 2^32 1845504
 
$ file zImage
zImage: Linux kernel ARM boot executable zImage (little-endian)

The file system could be extracted with abootimg-unpack-initrd which is part of the abootimg Debian package:

$ abootimg-unpack-initrd initrd.img ramdisk
3605 Blöcke
 
$ ls ramdisk/
charger                  init.qcom.sh                oem
data                     init.qcom.ssr.sh            proc
default.prop             init.qcom.syspart_fixup.sh  property_contexts
dev                      init.razer.info.sh          res
file_contexts            init.razer.peripherals.sh   sbin
fstab.qcom               init.razer.ping.sh          seapp_contexts
init                     init.razer.rc               selinux_version
init.class_main.sh       init.razer.usb.rc           sepolicy
init.environ.rc          init.razer.usb.sh           service_contexts
init.mdm.sh              init.rc                     sys
init.pearlyn.diag.rc     init.target.rc              system
init.qcom.class_core.sh  init.trace.rc               ueventd.qcom.rc
init.qcom.early_boot.sh  init.usb.configfs.rc        ueventd.rc
init.qcom.factory.sh     init.usb.rc
init.qcom.rc             init.zygote32.rc

Re-packing boot.img

After modifying default.prop to enable adb (more on that later) I had to regenerate a boot.img file and used abootimg-pack-initrd and mkbootimg.py:

$ abootimg-pack-initrd newinitrd.img ramdisk
$ ../mkbootimg.py --kernel cleanboot/zImage --ramdisk cleanboot/newinitrd.img --cmdline "console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0 androidboot.hardware=qcom user_debug=31 msm_rtb.filter=0x3b7 dwc3_msm.cpu_to_affin=1" --pagesize 0x1000 --kernel_offset 0x8000 --ramdisk_offset 0x1000000 --tags_offset 0x100  -o rebuilt-boot.img

Then I unplugged the Forge's HDMI cable and powered it on, which puts it into Fastboot mode.

Instead of flashing the whole firmware image .zip file with fastboot -w update image-forge-m-144.zip I only flashed the new boot.img file:

$ fastboot devices
171256710321511	 fastboot
$ fastboot flash boot rebuilt-boot.img
$ fastboot reboot

Boot problem

I was very suprised that the Forge did not boot. It was stuck in a loop: Try to boot (ca. 1 minute), reboot and try again.

Via systematic tests I found that my mkbootimg.py command was correct, but the new initrd.img was broken - or at least not understood by the Forge TV.

Finding a difference

The initrd.img is a gzip compress file, so I unpacked it first:

$ cp initrd.img initrd.cpio.gz
$ cp newinitrd.img newinitrd.cpio.gz
$ gunzip initrd.cpio.gz
$ gunzip newinitrd.cpio.gz

At first I looked at the file sizes:

$ ls *.cpio
1845504 17. Sep 18:15 initrd.cpio
1845760 17. Sep 18:31 newinitrd.cpio

The new image is a bit larger. Comparing the files with dhex showed massive differences.

Listing the contents of the .cpio archive showed that the new image has the local directory inside, which the original one had not:

$ cat initrd.cpio|cpio -t
charger
data
default.prop
[...]
 
$ cat newinitrd.cpio|cpio -t
.
charger
data
default.prop
[...]

Since abootimg-pack-initrd only uses cpio inside, I built my own cpio command chain to remove the dot:

$ cd ramdisk
$ find |grep -v '^.$' | sort | cpio --quiet -o -H newc > ../newinitrd2.cpio
$ cat ../newinitrd2.cpio|cpio -t
charger
data
default.prop
[...]

Comparing the .cpio files with dhex showed much less differences:

Comparing two cpio archives with dhex

An image built on that file did not boot, though.

Diffing .cpio archives

I did not want to learn the CPIO archive format and looked for a tool that could analyze them.

At first I tried ofrak, but it only showed header information and nothing about the file contents or the archive structure:

$ ofrak identify initrd.cpio
[   ofrak_cli.py:  173] No disassembler backend specified, so no disassembly will be possible
Identifying file: initrd.cpio
 
= Tags =
  CpioFilesystem
  FilesystemRoot
  GenericBinary
  File
  FilesystemEntry
= Attributes =
  AttributesType[FilesystemEntry](name=initrd.cpio, stat=os.stat_result(st_mode=33188, st_ino=10759612, st_dev=65027, st_nlink=1, st_uid=1000, st_gid=1000, st_size=1845504, st_atime=1695707951, st_mtime=1695707778, st_ctime=1695707931), xattrs={})  Magic:
   Mime: application/x-cpio
   Descriptor: ASCII cpio archive (SVR4 with no CRC)
 
It took 0.009 seconds to run the OFRAK script

Then I remembered binwalk and indeed got a nice overview on the cpio contents:

$ binwalk initrd.cpio
 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ASCII cpio archive (SVR4 with no CRC), file name: "charger", file name length: "0x00000008", file size: "0x0000000d"
136           0x88            ASCII cpio archive (SVR4 with no CRC), file name: "data", file name length: "0x00000005", file size: "0x00000000"
252           0xFC            ASCII cpio archive (SVR4 with no CRC), file name: "default.prop", file name length: "0x0000000d", file size: "0x0000023f"
[...]
 
$ binwalk newinitrd2.cpio
 
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ASCII cpio archive (SVR4 with no CRC), file name: "charger", file name length: "0x00000008", file size: "0x0000000D"
136           0x88            ASCII cpio archive (SVR4 with no CRC), file name: "data", file name length: "0x00000005", file size: "0x00000000"
252           0xFC            ASCII cpio archive (SVR4 with no CRC), file name: "default.prop", file name length: "0x0000000D", file size: "0x0000023F"
[...]

One difference that struck me was that the file size was given in lowercase hex letters in the original file (0x0000000d), while it was in uppercase in my custom archive (0x0000000D).

I had no idea if that is covered by the specification, but it could be the source of the problem. Now I had to find a way to generate a cpio archive with lowercase file size indicators.

pax

cpio was original part of the POSIX standard, but was abandoned in favor of pax. pax can read and generate cpio archives - and it shows more meta data than even binwalk!

$ pax -vf initrd.cpio
lrw-r--r--  1 root     root            13 Jan  1  1970 charger -> /sbin/healthd
drwxrwx--x  1 root     root             0 Jan  1  1970 data
-rw-r--r--  1 root     root           575 Jan  1  1970 default.prop
[...]
 
$ pax -vf newinitrd2.cpio
lrwxrwxrwx  1 cweiske  cweiske         13 Sep 26 07:25 charger -> /sbin/healthd
drwxrwx--x  2 cweiske  cweiske          0 Sep 26 07:25 data
-rw-r--r--  1 cweiske  cweiske        575 Sep 26 07:25 default.prop

This was the tool I had hoped for when I began finding the differences, and now they were very clear: My new file had my username and group, while the original one only "root". My archive had file modification timestamps, the original only a "0".

Generating the cpio archive with pax generated a file much larger than the original one:

$ find |grep -v '^.$' | sed 's#^./##' | sort | pax -w -x sv4cpio -M mtime -M uidgid > ../paxinitrd.cpio
$ ls -l *.cpio
1845504 26. Sep 07:56 initrd.cpio
1845760 26. Sep 08:07 newinitrd2.cpio
2575360 26. Sep 08:30 paxinitrd.cpio

It turned out that pax added duplicates:

$ pax -vf paxinitrd.cpio
[...]
drwxr-xr-x  3 root     root             0 Jan  1  1970 res/images
drwxr-xr-x  2 root     root             0 Jan  1  1970 res/images/charger
-rw-r--r--  1 root     root           463 Jan  1  1970 res/images/charger/battery_scale.png
-rw-r--r--  1 root     root          1368 Jan  1  1970 res/images/charger/battery_fail.png
drwxr-xr-x  3 root     root             0 Jan  1  1970 res/images
drwxr-xr-x  2 root     root             0 Jan  1  1970 res/images/charger
-rw-r--r--  1 root     root           463 Jan  1  1970 res/images/charger/battery_scale.png
-rw-r--r--  1 root     root          1368 Jan  1  1970 res/images/charger/battery_fail.png
drwxr-xr-x  2 root     root             0 Jan  1  1970 res/images/charger
-rw-r--r--  1 root     root           463 Jan  1  1970 res/images/charger/battery_scale.png
-rw-r--r--  1 root     root          1368 Jan  1  1970 res/images/charger/battery_fail.png
-rw-r--r--  1 root     root          1368 Jan  1  1970 res/images/charger/battery_fail.png
-rw-r--r--  1 root     root           463 Jan  1  1970 res/images/charger/battery_scale.png
[...]

The culprit for the duplicate files was pax' handling of directory names: find lists files and directories. pax then adds the files to the archive, but when it sees a directory it automatically adds all the files on its own - even though find lists the directory contents itself.

The -d option helps here - it causes pax to add the directory entry, but not the files inside:

$ find |grep -v '^.$' | sort| pax -w -x sv4cpio -M mtime -M uidgid -s '#^./##' -d -f ../paxinitrd2.cpio

The file was a tiny bit smaller than the original initrd.cpio, and the binwalk output was identical! dhex showed less differences than before:

Comparing two cpio archives with dhex #2

And what's most important: The Razer Forge TV could boot that boot.img file!


The story continues in Android TV: Skip Google Sign-In.

Written by Christian Weiske.

Comments? Please send an e-mail.