<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael</title>
    <description>The latest articles on DEV Community by Michael (@dokmic).</description>
    <link>https://dev.to/dokmic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2317142%2Ff1430319-e5f3-48b0-a9ce-ea52b02f18be.jpeg</url>
      <title>DEV Community: Michael</title>
      <link>https://dev.to/dokmic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dokmic"/>
    <language>en</language>
    <item>
      <title>Turn the region-locked Aqara G2H Camera into a global one</title>
      <dc:creator>Michael</dc:creator>
      <pubDate>Fri, 22 Aug 2025 21:01:31 +0000</pubDate>
      <link>https://dev.to/dokmic/turn-the-region-locked-aqara-g2h-camera-into-a-global-one-3p0c</link>
      <guid>https://dev.to/dokmic/turn-the-region-locked-aqara-g2h-camera-into-a-global-one-3p0c</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;I have quite some devices from Aqara in my household. Some of them I ordered before they became available in local stores, and they turned out to be region-locked.&lt;/p&gt;

&lt;p&gt;Before recently, they were all bound to the Chinese region in the Aqara app. After getting the new G100 version, I was unable to bind it in the Chinese region, as the global version is intended for all other regions except China.&lt;/p&gt;

&lt;h2&gt;
  
  
  Region Lock
&lt;/h2&gt;

&lt;p&gt;Long story short, I decided to move all my Aqara devices to a single region so I can control their settings without constantly switching between regions.&lt;/p&gt;

&lt;p&gt;The process went smoothly, but I've got a few G2Hs failing with error 668, stating that the device is not intended for use in the selected region.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fulosr5ygufsd9q6030p3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fulosr5ygufsd9q6030p3.png" alt="Error 668 in the Aqara Home app" title="Error 668 in the Aqara Home app" width="800" height="1601"&gt;&lt;/a&gt;&lt;br&gt;Error in the Aqara Home app when binding a region-locked device.
  &lt;/p&gt;

&lt;p&gt;There was no way to work around this by adding the camera to Apple Home first or by unbinding it from one region and rebinding it in another.&lt;/p&gt;

&lt;p&gt;The only difference we can see between the global and Chinese versions is the model number in the Apple Home app. The region-locked version is identified as &lt;code&gt;ZNSXJ12LM&lt;/code&gt;, whereas the global one displays &lt;code&gt;CH-H01&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;td&gt;
    
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk0ijg7g5r90kmlai9y3.png" alt="Chinese version in Apple Home" title="Chinese version in Apple Home" width="800" height="666"&gt;Chinese version in Apple Home.
      
    
  &lt;/td&gt;
  &lt;td&gt;
    
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbcfyk8tvwq3qj2k0icq.png" alt="Global version in Apple Home" title="Global version in Apple Home" width="800" height="672"&gt;Global version in Apple Home.
      
    
  &lt;/td&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hacking Camera
&lt;/h2&gt;

&lt;p&gt;After a quick search, I discovered the &lt;a href="https://github.com/mcchas/g2h-camera-mods" rel="noopener noreferrer"&gt;mcchas/g2h-camera-mods&lt;/a&gt; repository, which contains some tweaks for the G2H camera. The author did a great job finding a way to get root access.&lt;/p&gt;

&lt;p&gt;All you need to get remote access to the camera is to run the following script on your SD card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
#!/bin/sh

passwd -d root
echo WITH_TELNET=y &amp;gt;&amp;gt;/etc/.config

mv /mnt/sdcard/hostname /mnt/sdcard/hostname.bak
reboot
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rebooting, the camera became available via telnet. It's running Linux and has enough basic commands for experimenting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing Model
&lt;/h2&gt;

&lt;p&gt;First, I checked if there were occurrences of the model number in the filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ZNSXJ12LM /
/etc/build.prop:ro.sys.product&lt;span class="o"&gt;=&lt;/span&gt;ZNSXJ12LM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That seems like a build parameter, and changing this string in &lt;code&gt;/etc/build.prop&lt;/code&gt; obviously had no effect.&lt;/p&gt;

&lt;p&gt;After checking the environment, I discovered some commands used to gather camera information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;get_
get_dev_status    get_homekit_info  get_lens          get_model         get_sn            get_zig_chipid    get_zig_ver
get_hd_ver        get_language      get_lumi_info     get_product_info  get_soft_ver      get_zig_mac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is another set to update it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;set_
set_hd_ver        set_homekit_info  set_language      set_led_b         set_led_r         set_lens          set_lumi_info     set_product_info  set_sn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After checking some of them, &lt;code&gt;get_product_info&lt;/code&gt; seemed to be the one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;get_product_info
product: ZNSXJ12LM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the corresponding pair seemed to do the job, updating the model number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;set_product_info CH-H01
set_product_info: ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rebooting, the camera started showing the updated model number in Apple Home, but unfortunately, it still did not allow it to bind in the desired region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing Internal Model
&lt;/h2&gt;

&lt;p&gt;I checked the build parameters once more, and apart from the model number, there was also a model name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/build.prop
ro.sys.name&lt;span class="o"&gt;=&lt;/span&gt;Camera-Hub-G2H
ro.sys.model&lt;span class="o"&gt;=&lt;/span&gt;lumi.camera.gwagl02
ro.sys.product&lt;span class="o"&gt;=&lt;/span&gt;ZNSXJ12LM
ro.sys.spu&lt;span class="o"&gt;=&lt;/span&gt;AC004
ro.sys.sku&lt;span class="o"&gt;=&lt;/span&gt;000
ro.sys.ean13&lt;span class="o"&gt;=&lt;/span&gt;6970504211889
ro.sys.manufacturer&lt;span class="o"&gt;=&lt;/span&gt;Aqara
ro.sys.vendor&lt;span class="o"&gt;=&lt;/span&gt;Lumi United Technology Co., Ltd.
ro.sys.fw_ver&lt;span class="o"&gt;=&lt;/span&gt;2.2.7
ro.sys.hw_ver&lt;span class="o"&gt;=&lt;/span&gt;1.0
ro.sys.build_num&lt;span class="o"&gt;=&lt;/span&gt;0001
ro.sys.acc_tags&lt;span class="o"&gt;=&lt;/span&gt;red
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick search showed that &lt;code&gt;lumi.camera.gwagl02&lt;/code&gt; corresponds to a Chinese revision and &lt;code&gt;lumi.camera.gwag03&lt;/code&gt; to a global version.&lt;/p&gt;

&lt;p&gt;Similarly, there is a &lt;code&gt;get_model&lt;/code&gt; command returning the model name, but there is no &lt;code&gt;set_model&lt;/code&gt; to override it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;get_model
model: lumi.camera.gwagl02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All these getter and setter commands are symlinks to the same binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /local/bin | &lt;span class="nb"&gt;grep &lt;/span&gt;get_model
lrwxrwxrwx    1 1020     1020            12 get_model -&amp;gt; factory_test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried to reverse engineer this binary using &lt;a href="https://github.com/NationalSecurityAgency/ghidra" rel="noopener noreferrer"&gt;ghidra&lt;/a&gt;. After searching for the string &lt;code&gt;model:&lt;/code&gt;, I've got the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8l7lxpbeig7lq8kkgfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8l7lxpbeig7lq8kkgfs.png" alt="Template string from the  raw `get_model` endraw  command" title="Template string from the  raw `get_model` endraw  command" width="800" height="581"&gt;&lt;/a&gt;&lt;br&gt;Template string from the &lt;code&gt;get_model&lt;/code&gt; command.
  &lt;/p&gt;

&lt;p&gt;Unfortunately, it failed to disassemble some of the functions, including the one printing this string. But after checking the surroundings, I noticed this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mwjsbz60jfywfghmzt4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mwjsbz60jfywfghmzt4.png" alt="Potential config file" title="Potential config file" width="800" height="640"&gt;&lt;/a&gt;&lt;br&gt;Potential config file.
  &lt;/p&gt;

&lt;p&gt;This binary is using &lt;code&gt;/mnt/config/miio/device.conf&lt;/code&gt; for some purposes, which has something interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /mnt/config/miio/device.conf | &lt;span class="nb"&gt;grep &lt;/span&gt;lumi.camera
&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lumi.camera.gwagl02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after changing the model line, I finally got &lt;code&gt;get_model&lt;/code&gt; returning what's needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-ie&lt;/span&gt; &lt;span class="s1"&gt;'s/model=.*/model=lumi.camera.gwag03/'&lt;/span&gt; /mnt/config/miio/device.conf
&lt;span class="nv"&gt;$ &lt;/span&gt;get_model
model: lumi.camera.gwag03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after rebooting, I was finally able to add my camera to another region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;In the end, I needed only two commands to remove the region lock on my camera. Those commands can be applied automatically using the &lt;code&gt;hostname&lt;/code&gt; hack.&lt;/p&gt;

&lt;p&gt;For that, just put a file named &lt;code&gt;hostname&lt;/code&gt; on your SD card with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-ie&lt;/span&gt; &lt;span class="s1"&gt;'s/model=.*/model=lumi.camera.gwag03/'&lt;/span&gt; /mnt/config/miio/device.conf
set_product_info CH-H01

&lt;span class="nb"&gt;mv&lt;/span&gt; /mnt/sdcard/hostname /mnt/sdcard/hostname.bak
reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hope this helps someone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Translations of this article are allowed only upon permission.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Running Raspberry Pi OS in a Docker Container</title>
      <dc:creator>Michael</dc:creator>
      <pubDate>Mon, 25 Nov 2024 09:00:54 +0000</pubDate>
      <link>https://dev.to/dokmic/running-raspberry-pi-os-in-a-docker-container-4p4d</link>
      <guid>https://dev.to/dokmic/running-raspberry-pi-os-in-a-docker-container-4p4d</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Sometimes, testing your work on the Raspberry Pi OS is much easier without running it on real hardware. Things like Ansible Playbooks or a Kubernetes cluster, in most cases, can be tested in a virtualized environment.&lt;/p&gt;

&lt;p&gt;There are plenty of tutorials and other Docker images running Raspberry Pi OS using &lt;a href="https://www.qemu.org/" rel="noopener noreferrer"&gt;QEMU&lt;/a&gt;, but unfortunately, all of them utilize the OS image at runtime as an SD card. Hence, they do not support mounting volumes to share the filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image
&lt;/h2&gt;

&lt;p&gt;The most obvious solution would be to extract the OS image and share the extracted folder with the virtual machine. Fortunately, QEMU provides such an option out of the box with the &lt;code&gt;-virtfs&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-virtfs local,path=path,mount_tag=mount_tag ,security_model=security_model[,writeout=writeout][,readonly=on] [,fmode=fmode][,dmode=dmode][,multidevs=multidevs]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's now try to download and extract all the OS files from a Raspberry Pi OS distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-10-28/2024-10-22-raspios-bookworm-arm64-lite.img.xz &lt;span class="se"&gt;\&lt;/span&gt;
| unxz &lt;span class="nt"&gt;-c&lt;/span&gt; - &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/sd.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted image can be inspected with the &lt;a href="https://linux.die.net/man/8/fdisk" rel="noopener noreferrer"&gt;&lt;code&gt;fdisk&lt;/code&gt;&lt;/a&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;fdisk &lt;span class="nt"&gt;-l&lt;/span&gt; /tmp/sd.img 
Disk /tmp/sd.img: 2732 MB, 2864709632 bytes, 5595136 sectors
43712 cylinders, 4 heads, 32 sectors/track
Units: sectors of 1 &lt;span class="k"&gt;*&lt;/span&gt; 512 &lt;span class="o"&gt;=&lt;/span&gt; 512 bytes

Device     Boot StartCHS    EndCHS        StartLBA     EndLBA    Sectors  Size Id Type
/tmp/sd.img1    64,0,1      1023,3,32         8192    1056767    1048576  512M  c Win95 FAT32 &lt;span class="o"&gt;(&lt;/span&gt;LBA&lt;span class="o"&gt;)&lt;/span&gt;
/tmp/sd.img2    1023,3,32   1023,3,32      1056768    5595135    4538368 2216M 83 Linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, the image has two partitions. The first one, the boot partition, is of type &lt;code&gt;FAT32&lt;/code&gt;, and the second is of type &lt;code&gt;ext4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can mount those partitions using the &lt;a href="https://en.wikipedia.org/wiki/Loop_device#Uses_of_loop_mounting" rel="noopener noreferrer"&gt;&lt;code&gt;loop&lt;/code&gt;&lt;/a&gt; device with &lt;a href="https://linux.die.net/man/8/mount" rel="noopener noreferrer"&gt;&lt;code&gt;mount&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://linux.die.net/man/8/sfdisk" rel="noopener noreferrer"&gt;&lt;code&gt;sfdisk&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://jqlang.github.io/jq/" rel="noopener noreferrer"&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/a&gt; in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop,offset&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sfdisk &lt;span class="nt"&gt;-J&lt;/span&gt; /tmp/sd.img | jq &lt;span class="s1"&gt;'.partitiontable.sectorsize * .partitiontable.partitions[1].start'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/sd.img /tmp/sd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining those all together in a &lt;code&gt;Dockerfile&lt;/code&gt; would look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1.3-labs&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;

&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-10-28/2024-10-22-raspios-bookworm-arm64-lite.img.xz /sd.img.xz&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--security&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;insecure &lt;span class="se"&gt;\
&lt;/span&gt;  apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--virtual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.tools &lt;span class="se"&gt;\
&lt;/span&gt;    jq &lt;span class="se"&gt;\
&lt;/span&gt;    sfdisk &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unxz &lt;span class="nt"&gt;-ck&lt;/span&gt; /sd.img.xz &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/sd.img &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop,offset&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sfdisk &lt;span class="nt"&gt;-J&lt;/span&gt; /tmp/sd.img | jq &lt;span class="s1"&gt;'.partitiontable.sectorsize * .partitiontable.partitions[1].start'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/sd.img /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop,offset&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sfdisk &lt;span class="nt"&gt;-J&lt;/span&gt; /tmp/sd.img | jq &lt;span class="s1"&gt;'.partitiontable.sectorsize * .partitiontable.partitions[0].start'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/sd.img /tmp/sd/boot/firmware &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-pr&lt;/span&gt; /tmp/sd /media/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; umount /tmp/sd/boot/firmware /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/sd /tmp/sd.img &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mounting volumes requires a privileged mode. That is why we need the &lt;code&gt;--security=insecure&lt;/code&gt; flag after the &lt;code&gt;RUN&lt;/code&gt; instruction, which is only available in the labs specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kernel
&lt;/h2&gt;

&lt;p&gt;Now, if we try to run the extracted image using QEMU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-system-aarch64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-serial&lt;/span&gt; mon:stdio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-nographic&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-no-reboot&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-machine&lt;/span&gt; virt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-cpu&lt;/span&gt; cortex-a72 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; 1G &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-smp&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-device&lt;/span&gt; virtio-net-device,netdev&lt;span class="o"&gt;=&lt;/span&gt;net0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-netdev&lt;/span&gt; user,id&lt;span class="o"&gt;=&lt;/span&gt;net0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-kernel&lt;/span&gt; /media/sd/boot/firmware/kernel8.img &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-virtfs&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt;,id&lt;span class="o"&gt;=&lt;/span&gt;boot,mount_tag&lt;span class="o"&gt;=&lt;/span&gt;boot,multidevs&lt;span class="o"&gt;=&lt;/span&gt;remap,path&lt;span class="o"&gt;=&lt;/span&gt;/media/sd/boot/firmware,security_model&lt;span class="o"&gt;=&lt;/span&gt;none &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-virtfs&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt;,id&lt;span class="o"&gt;=&lt;/span&gt;root,mount_tag&lt;span class="o"&gt;=&lt;/span&gt;root,multidevs&lt;span class="o"&gt;=&lt;/span&gt;remap,path&lt;span class="o"&gt;=&lt;/span&gt;/media/sd,security_model&lt;span class="o"&gt;=&lt;/span&gt;none &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-append&lt;/span&gt; &lt;span class="s2"&gt;"console=ttyAMA0,115200 root=root rootflags=cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L rootfstype=9p rootwait"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel will throw a panic since it cannot mount the root of type &lt;code&gt;9p&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[    1.144617] Disabling rootwait; root= is invalid.
[    1.153539] VFS: Cannot open root device "root" or unknown-block(0,0): error -19
[    1.154459] Please append a correct "root=" boot option; here are the available partitions:
...
[    1.156259] List of all bdev filesystems:
[    1.156335]  ext3
[    1.156346]  ext2
[    1.156386]  ext4
[    1.156414]  vfat
[    1.156440]  msdos
[    1.156469]  f2fs
[    1.156497] 
[    1.156625] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the &lt;code&gt;9pfs&lt;/code&gt; support is present in the Linux kernel, the Raspberry Pi OS &lt;a href="https://github.com/raspberrypi/linux" rel="noopener noreferrer"&gt;kernel&lt;/a&gt; was not built with the corresponding &lt;a href="https://wiki.qemu.org/Documentation/9psetup#Preparation" rel="noopener noreferrer"&gt;flags&lt;/a&gt;. Let's try to fix it by rebuilding the kernel ourselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ubuntu:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;kernel&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ARCH=arm64&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CROSS_COMPILE=aarch64-linux-gnu-&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /tmp/kernel&lt;/span&gt;

&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://github.com/raspberrypi/linux/archive/refs/tags/stable_20241008.tar.gz /kernel.tar.gz&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-xzf&lt;/span&gt; /kernel.tar.gz &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--mark-auto&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    bc &lt;span class="se"&gt;\
&lt;/span&gt;    bison &lt;span class="se"&gt;\
&lt;/span&gt;    flex &lt;span class="se"&gt;\
&lt;/span&gt;    gcc &lt;span class="se"&gt;\
&lt;/span&gt;    gcc-aarch64-linux-gnu &lt;span class="se"&gt;\
&lt;/span&gt;    libc6-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libc6-dev-arm64-cross &lt;span class="se"&gt;\
&lt;/span&gt;    libssl-dev &lt;span class="se"&gt;\
&lt;/span&gt;    make &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nv"&gt;O&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/build defconfig &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; scripts/config &lt;span class="nt"&gt;--file&lt;/span&gt; /tmp/build/.config &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS_POSIX_ACL y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS_SECURITY y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NETWORK_FILESYSTEMS y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NET_9P y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NET_9P_VIRTIO y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI_HOST_COMMON y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI_HOST_GENERIC y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_PCI y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_BLK y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_NET y &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nv"&gt;O&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/build &lt;span class="nt"&gt;-j&lt;/span&gt; 3 Image.gz &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/kernel &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /media/sd/boot/firmware &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/build/arch/&lt;span class="nv"&gt;$ARCH&lt;/span&gt;/boot/Image.gz /media/sd/boot/firmware/kernel8.img
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Now, the command above should boot the operating system, which still fails to initialize completely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FAILED] Failed to start systemd-re…ount Root and Kernel File Systems.
See 'systemctl status systemd-remount-fs.service' for details.
[ TIME ] Timed out waiting for device /dev/disk/by-partuuid/385cce61-01.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is failing due to incorrect records in &lt;code&gt;/etc/fstab&lt;/code&gt; as we provide the filesystem over &lt;code&gt;9pfs&lt;/code&gt;. That can be fixed by overwriting the prebaked &lt;code&gt;/etc/fstab&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&amp;lt;EOF /media/sd/etc/fstab&lt;/span&gt;
proc /proc proc defaults 0 0
boot /boot/firmware 9p cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L 0 2
root / 9p cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L 0 1
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also makes sense to update the boot options in &lt;code&gt;cmdline.txt&lt;/code&gt; so we can reuse this file and thereby simulate Raspberry Pi's behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&amp;lt;EOF /media/sd/boot/firmware/cmdline.txt&lt;/span&gt;
console=ttyAMA0,115200 init=/usr/lib/raspberrypi-sys-mods/firstboot root=root rootflags=cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L rootfstype=9p rootwait
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can finally boot the OS and log in. However, a couple of services are still failing to initialize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FAILED] Failed to start rpi-eeprom…k for Raspberry Pi EEPROM updates.
[FAILED] Failed to start resize2fs_…root filesystem to fill partition.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we are running in a virtualized environment, there is no need to run &lt;code&gt;rpi-eeprom-update.service&lt;/code&gt; which is attempting to update Raspberry Pi's firmware.  The second service tries to expand the root filesystem, but it fails since we are accessing this over 9P. Let's disable both of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  /media/sd/etc/init.d/resize2fs_once &lt;span class="se"&gt;\
&lt;/span&gt;  /media/sd/etc/systemd/system/multi-user.target.wants/rpi-eeprom-update.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hardware
&lt;/h2&gt;

&lt;p&gt;QEMU &lt;a href="https://www.qemu.org/docs/master/system/arm/raspi.html" rel="noopener noreferrer"&gt;provides&lt;/a&gt; emulation of Raspberry Pi boards out of the box. Unfortunately, it is not so performant when running from Docker, especially since Docker is also running in a virtualized environment on MacOS. On top of that, it does not support PCI devices, which are required for the filesystem sharing.&lt;/p&gt;

&lt;p&gt;Instead, we can use the generic platform &lt;code&gt;virt&lt;/code&gt;, which is &lt;a href="https://www.qemu.org/docs/master/system/arm/virt.html" rel="noopener noreferrer"&gt;optimized&lt;/a&gt; to run in a virtualized environment like Docker for Mac.&lt;/p&gt;

&lt;p&gt;In this case, we should explicitly specify the CPU (&lt;code&gt;-smp 4&lt;/code&gt;) and RAM (&lt;code&gt;-m 1G&lt;/code&gt;) resources available for the virtual machine. It also makes sense to stick to the &lt;code&gt;cortex-a72&lt;/code&gt; CPU as it is being used on an actual board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entrypoint
&lt;/h2&gt;

&lt;p&gt;The last part is to get support for the &lt;code&gt;cmdline.txt&lt;/code&gt; as Raspberry Pi OS relies on this file. At this moment, the contents of this file will be restored upon container restart. So that, if we change the &lt;code&gt;-append&lt;/code&gt; parameter to read from the file, it does not help much:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-append "$(cat /media/sd/boot/firmware/cmdline.txt)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The QEMU command should be wrapped into a script to restart the virtual machine on soft reboots. In this case, the OS or a user can modify the &lt;code&gt;cmdline.txt&lt;/code&gt; and apply these changes in the current container.&lt;/p&gt;

&lt;p&gt;There is no way to intercept user reboots and differentiate them from kernel panics. The only possible option would be to read the serial port and restart the process when the kernel yields &lt;code&gt;reboot: Restarting system&lt;/code&gt;. That is achievable using &lt;a href="https://linux.die.net/man/1/expect" rel="noopener noreferrer"&gt;&lt;code&gt;expect&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chmod=755 &amp;lt;&amp;lt;'EOF' /usr/local/bin/rpi&lt;/span&gt;
&lt;span class="c"&gt;#!/usr/bin/expect&lt;/span&gt;

while {true} {
  set reboot false

  spawn -noecho qemu-system-aarch64 \
    -serial mon:stdio \
    -nographic \
    -no-reboot \
    -machine virt \
    -cpu cortex-a72 \
    -m 1G \
    -smp 4 \
    -device virtio-net-device,netdev=net0 \
    -netdev user,id=net0 \
    -kernel /media/sd/boot/firmware/kernel8.img \
    -virtfs local,id=boot,mount_tag=boot,multidevs=remap,path=/media/sd/boot/firmware,security_model=none \
    -virtfs local,id=root,mount_tag=root,multidevs=remap,path=/media/sd,security_model=none \
    -append "[exec cat /media/sd/boot/firmware/cmdline.txt] panic=-1"

  interact {
    -o -reset

    -nobuffer "reboot: Restarting system" {
      set reboot true
      expect eof
      return
    }
  }

  lassign [wait] pid spawn_id os_error exit_code

  if {$exit_code != 0 || !$reboot} {
    exit $exit_code
  }
}
EOF

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["rpi"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the script above, we modified the &lt;code&gt;-append&lt;/code&gt; argument to add the &lt;code&gt;panic=-1&lt;/code&gt; kernel parameter. In this case, QEMU exits on reboot, and the &lt;code&gt;expect&lt;/code&gt; script will reread &lt;code&gt;cmdline.txt&lt;/code&gt;. So the OS or a user can make modifications in that file and then reboot to pick up the changes just like on a normal Raspberry Pi board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Here is the complete Dockerfile including all the instructions above:&lt;/p&gt;

&lt;p&gt;
  Dockerfile
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1.3-labs&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;

&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-10-28/2024-10-22-raspios-bookworm-arm64-lite.img.xz /sd.img.xz&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--security&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;insecure &lt;span class="se"&gt;\
&lt;/span&gt;  apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;--virtual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.tools &lt;span class="se"&gt;\
&lt;/span&gt;    jq &lt;span class="se"&gt;\
&lt;/span&gt;    sfdisk &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unxz &lt;span class="nt"&gt;-ck&lt;/span&gt; /sd.img.xz &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/sd.img &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop,offset&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sfdisk &lt;span class="nt"&gt;-J&lt;/span&gt; /tmp/sd.img | jq &lt;span class="s1"&gt;'.partitiontable.sectorsize * .partitiontable.partitions[1].start'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/sd.img /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mount &lt;span class="nt"&gt;-o&lt;/span&gt; loop,offset&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sfdisk &lt;span class="nt"&gt;-J&lt;/span&gt; /tmp/sd.img | jq &lt;span class="s1"&gt;'.partitiontable.sectorsize * .partitiontable.partitions[0].start'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/sd.img /tmp/sd/boot/firmware &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-pr&lt;/span&gt; /tmp/sd /media/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; umount /tmp/sd/boot/firmware /tmp/sd &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/sd /tmp/sd.img &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apk del .tools

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  /media/sd/etc/init.d/resize2fs_once &lt;span class="se"&gt;\
&lt;/span&gt;  /media/sd/etc/systemd/system/multi-user.target.wants/rpi-eeprom-update.service

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&amp;lt;EOF /media/sd/etc/fstab&lt;/span&gt;
proc /proc proc defaults 0 0
boot /boot/firmware 9p cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L 0 2
root / 9p cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L 0 1
EOF

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;&amp;lt;EOF /media/sd/boot/firmware/cmdline.txt&lt;/span&gt;
console=ttyAMA0,115200 init=/usr/lib/raspberrypi-sys-mods/firstboot root=root rootflags=cache=mmap,msize=104857600,posixacl,trans=virtio,version=9p2000.L rootfstype=9p rootwait
EOF

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ubuntu:latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;kernel&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ARCH=arm64&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CROSS_COMPILE=aarch64-linux-gnu-&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /tmp/kernel&lt;/span&gt;

&lt;span class="k"&gt;ADD&lt;/span&gt;&lt;span class="s"&gt; https://github.com/raspberrypi/linux/archive/refs/tags/stable_20241008.tar.gz /kernel.tar.gz&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-xzf&lt;/span&gt; /kernel.tar.gz &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get update &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--mark-auto&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    bc &lt;span class="se"&gt;\
&lt;/span&gt;    bison &lt;span class="se"&gt;\
&lt;/span&gt;    flex &lt;span class="se"&gt;\
&lt;/span&gt;    gcc &lt;span class="se"&gt;\
&lt;/span&gt;    gcc-aarch64-linux-gnu &lt;span class="se"&gt;\
&lt;/span&gt;    libc6-dev &lt;span class="se"&gt;\
&lt;/span&gt;    libc6-dev-arm64-cross &lt;span class="se"&gt;\
&lt;/span&gt;    libssl-dev &lt;span class="se"&gt;\
&lt;/span&gt;    make &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nv"&gt;O&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/build defconfig &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; scripts/config &lt;span class="nt"&gt;--file&lt;/span&gt; /tmp/build/.config &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS_POSIX_ACL y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_9P_FS_SECURITY y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NETWORK_FILESYSTEMS y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NET_9P y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_NET_9P_VIRTIO y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI_HOST_COMMON y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_PCI_HOST_GENERIC y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_PCI y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_BLK y &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--set-val&lt;/span&gt; CONFIG_VIRTIO_NET y &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nv"&gt;O&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/build &lt;span class="nt"&gt;-j&lt;/span&gt; 3 Image.gz &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/kernel &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /media/sd/boot/firmware &lt;span class="se"&gt;\
&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/build/arch/&lt;span class="nv"&gt;$ARCH&lt;/span&gt;/boot/Image.gz /media/sd/boot/firmware/kernel8.img

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:latest&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;  expect &lt;span class="se"&gt;\
&lt;/span&gt;  qemu-system-aarch64

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=image /media/sd /media/sd&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=kernel /media/sd /media/sd&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chmod=755 &amp;lt;&amp;lt;'EOF' /usr/local/bin/rpi&lt;/span&gt;
&lt;span class="c"&gt;#!/usr/bin/expect&lt;/span&gt;

while {true} {
  set reboot false

  spawn -noecho qemu-system-aarch64 \
    -serial mon:stdio \
    -nographic \
    -no-reboot \
    -machine virt \
    -cpu cortex-a72 \
    -m 1G \
    -smp 4 \
    -device virtio-net-device,netdev=net0 \
    -netdev user,id=net0 \
    -kernel /media/sd/boot/firmware/kernel8.img \
    -virtfs local,id=boot,mount_tag=boot,multidevs=remap,path=/media/sd/boot/firmware,security_model=none \
    -virtfs local,id=root,mount_tag=root,multidevs=remap,path=/media/sd,security_model=none \
    -append "[exec cat /media/sd/boot/firmware/cmdline.txt] panic=-1"

  interact {
    -o -reset

    -nobuffer "reboot: Restarting system" {
      set reboot true
      expect eof
      return
    }
  }

  lassign [wait] pid spawn_id os_error exit_code

  if {$exit_code != 0 || !$reboot} {
    exit $exit_code
  }
}
EOF

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["rpi"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;It lacks some features, like enabling SSH and changing the default user password, but those can be found in other tutorials or the official &lt;a href="https://www.raspberrypi.com/documentation/computers/configuration.html#remote-access" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Despite that, it has already been implemented and tested in the &lt;a href="https://github.com/dokmic/docker-rpi" rel="noopener noreferrer"&gt;original repository&lt;/a&gt;, which prompted me to write this article. And of course, there is a &lt;a href="https://hub.docker.com/r/dokmic/rpi" rel="noopener noreferrer"&gt;&lt;code&gt;dokmic/rpi&lt;/code&gt;&lt;/a&gt; ready-to-use image published on Docker Hub that supports both ARM64 and ARM architectures and has several configuration options to customize the emulated Raspberry Pi OS.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Translations of this article are allowed only upon permission.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>docker</category>
      <category>iot</category>
      <category>virtualmachine</category>
    </item>
  </channel>
</rss>
