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.
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.
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:
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:
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.