<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged arm64</title>
<link>https://coderlegion.com/tag/arm64</link>
<description>Powered by Question2Answer</description>
<item>
<title>How to Fix an ARM64 WAF Deploy When Personal Edition Is Not Supported</title>
<link>https://coderlegion.com/33280/how-to-fix-an-arm64-waf-deploy-when-personal-edition-is-not-supported</link>
<description>&lt;p&gt;An ARM64 host can meet every published dependency and still be configured as an unsupported SafeLine deployment. The concrete misconfiguration is a manual .env left on the sample values: ARCH_SUFFIX is empty, POSTGRES_IMAGE is chaitin/safeline-postgres:15.18, and the operator plans to run Personal Edition. The Deploy guide says an ARM server must set ARCH_SUFFIX=-arm and POSTGRES_IMAGE=chaitin/safeline-postgres-arm:15.18. It also states that SafeLine installed on ARM requires a Pro license to work properly, and that the Personal Edition is not supported on ARM (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id=&quot;confirm-the-host-before-editing-env&quot;&gt;Confirm the host before editing .env&lt;/h3&gt;
&lt;p&gt;SafeLine lists Linux, instruction architectures x86_64 and arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, and a minimum environment of 1 core CPU, 1 GB memory, and 5 GB disk (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). The same page gives the checks: uname -m for instruction architecture, docker version, docker compose version, free -h, and df -h. It also lists a CPU ssse3 check with lscpu | grep ssse3. Run uname -m first. If it reports an ARM architecture, do not keep the blank ARCH_SUFFIX from the sample .env. A blank suffix is the sample default, not the ARM setting.&lt;/p&gt;
&lt;p&gt;The Deploy guide also offers an automatic install command that requires root. This fix follows the manual path, because that is where ARCH_SUFFIX and POSTGRES_IMAGE are written by hand. Create the data directory with mkdir -p &quot;/data/safeline&quot;, download &lt;a rel=&quot;nofollow&quot; href=&quot;https://waf.chaitin.com/release/latest/compose.yaml&quot;&gt;https://waf.chaitin.com/release/latest/compose.yaml&lt;/a&gt; into that directory, and write .env there. The sample sets SAFELINE_DIR=/data/safeline, IMAGE_TAG=latest, MGT_PORT=9443, a Postgres password, SUBNET_PREFIX=172.22.222, IMAGE_PREFIX=chaitin, an empty ARCH_SUFFIX, an empty RELEASE, REGION=-g, and MGT_PROXY=0. On ARM, replace the architecture lines before the first launch. Set ARCH_SUFFIX=-arm and POSTGRES_IMAGE=chaitin/safeline-postgres-arm:15.18. Leave RELEASE empty unless you intentionally want the LTS build. The Deploy guide says the LTS version will no longer be maintained starting from 9.1.0-LTS (14 August 2025) and strongly recommends an upgrade to the latest version.&lt;/p&gt;
&lt;p&gt;Generate the documented secrets in the SafeLine directory, write DETECTOR_MGMT_TOKEN, MGT_INTERNAL_TOKEN, and DETECTOR_AES_KEY into .env, and chmod 600 .env. Those steps are separate from the architecture mistake, but a correct ARM file still needs them before docker compose up -d. Also create the documented resource directories and apply the documented permissions before launch, including chmod 700 on the run, mgt, sock, and postgres data paths named in the guide, plus the documented socket link.&lt;/p&gt;
&lt;h3 id=&quot;the-edition-limit-is-not-an-image-tag-typo&quot;&gt;The edition limit is not an image-tag typo&lt;/h3&gt;
&lt;p&gt;Changing the suffix does not remove the edition limit. The Deploy guide is explicit: SafeLine installed on ARM requires a Pro license to work properly, and the Personal Edition is not supported on ARM. If the only edition you can activate is Personal Edition, correcting ARCH_SUFFIX is not enough to make that edition supported. Treat a Personal Edition plan on this host as part of the same misconfiguration, not as a tuning choice. The supported path in that document is a Pro license together with the ARM image settings.&lt;/p&gt;
&lt;p&gt;The Deploy guide does not publish a sample denial log for a Personal Edition install on ARM. Do not treat a guessed error string as proof. Use the written requirement as the decision, then use the console screen below as proof that the corrected deploy is actually up.&lt;/p&gt;
&lt;h3 id=&quot;the-screen-that-confirms-the-fix&quot;&gt;The screen that confirms the fix&lt;/h3&gt;
&lt;p&gt;After ARCH_SUFFIX=-arm, the ARM Postgres image line, and a Pro license are in place, start the stack with docker compose up -d from /data/safeline. The Deploy guide says this can take several minutes. The confirmation is the web console the guide tells you to open. Browse to &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; and you should see the web console page. Then run docker exec safeline-mgt resetadmin. A successful run prints the initial username, the initial password, and a Done line. Keep that password and log in. That console page, together with those resetadmin lines, is the screen that confirms the architecture settings were applied and the service reached the documented ready state.&lt;/p&gt;
&lt;p&gt;If the console URL does not load, re-read .env before changing anything else. The two lines that define this misconfiguration are still the ones to check: ARCH_SUFFIX must be -arm, and POSTGRES_IMAGE must be chaitin/safeline-postgres-arm:15.18. Also confirm MGT_PORT is still 9443 unless you chose another console port, because the documented URL uses that port. MGT_PROXY should stay 0 unless you are configuring a proxy for the console. Re-check uname -m if you are no longer sure the host is ARM. The architecture check and the .env lines have to agree before the console can be treated as a fixed ARM deploy.&lt;/p&gt;
&lt;h3 id=&quot;after-the-console-is-up&quot;&gt;After the console is up&lt;/h3&gt;
&lt;p&gt;SafeLine is a self-hosted WAF placed in front of a web application. The product introduction says it filters and monitors HTTP traffic and can defend against SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Core capabilities listed there include defenses for web attacks, proactive bot abused defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list. The docs also state that SafeLine is production-ready. It works as a reverse proxy: clients pass through SafeLine before they reach the server, and SafeLine forwards traffic that passes policy (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Once login works, open the Applications page and click Add Application. Enter the domain, hostname, or IP, the listen port such as 80 or 443, check SSL for an HTTPS app, and set the upstream to the real address of the original app. Resolve that domain to the IP of the server where SafeLine runs. The configuration guide shows sample requests for SQL injection, XSS, path traversal, code injection, and XXE, and says a blocking page means the attack was defended. Open the console attack list and click detail for the event. That check proves protection on a live site. It does not replace the resetadmin screen that confirmed the ARM deploy itself.&lt;/p&gt;
&lt;h4 id=&quot;does-personal-edition-run-on-an-arm64-safeline-host-&quot;&gt;Does Personal Edition run on an ARM64 SafeLine host?&lt;/h4&gt;
&lt;p&gt;No. The Deploy guide says SafeLine installed on ARM requires a Pro license to work properly, and the Personal Edition is not supported on ARM (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-environment-lines-are-wrong-on-arm-if-i-keep-the-sample-file-&quot;&gt;Which environment lines are wrong on ARM if I keep the sample file?&lt;/h4&gt;
&lt;p&gt;ARCH_SUFFIX is empty in the sample, and the sample Postgres image is chaitin/safeline-postgres:15.18. On an ARM server those must become ARCH_SUFFIX=-arm and POSTGRES_IMAGE=chaitin/safeline-postgres-arm:15.18.&lt;/p&gt;
&lt;h4 id=&quot;what-output-confirms-the-corrected-deploy-&quot;&gt;What output confirms the corrected deploy?&lt;/h4&gt;
&lt;p&gt;The web console at &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; should load, and docker exec safeline-mgt resetadmin should print the initial username, the initial password, and Done.&lt;/p&gt;
&lt;h4 id=&quot;is-arm64-outside-the-published-architecture-list-&quot;&gt;Is arm64 outside the published architecture list?&lt;/h4&gt;
&lt;p&gt;No. The dependency table lists instruction architectures x86_64 and arm64. The command uname -m is the documented way to view the instruction architecture before you choose ARCH_SUFFIX.&lt;/p&gt;
&lt;h4 id=&quot;what-should-i-verify-before-the-first-docker-compose-up-&quot;&gt;What should I verify before the first docker compose up?&lt;/h4&gt;
&lt;p&gt;Confirm Docker is 20.10.14 or above, Docker Compose is 2.0.0 or above, and the host has at least 1 core, 1 GB memory, and 5 GB disk. On ARM, also confirm the two image lines and that you are not relying on Personal Edition.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33280/how-to-fix-an-arm64-waf-deploy-when-personal-edition-is-not-supported</guid>
<pubDate>Sun, 01 Nov 2026 03:46:59 +0000</pubDate>
</item>
<item>
<title>Why I Check ARM64 Edition Limits Before Turning SafeLine On</title>
<link>https://coderlegion.com/33279/why-i-check-arm64-edition-limits-before-turning-safeline-on</link>
<description>&lt;p&gt;I do not turn protection on until an ARM64 host has passed an edition check. Architecture support and edition support are different facts. The deploy guide lists both x86_64 and arm64, then states that SafeLine installed on ARM requires a Pro license to work properly and that the Personal Edition is not supported on ARM (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). If I treat arm64 as permission to use Personal Edition, I can finish Docker setup and still have an instance that is not allowed to do the job.&lt;/p&gt;
&lt;p&gt;This is the checklist I run before any public cutover.&lt;/p&gt;
&lt;h3 id=&quot;confirm-the-edition-before-touching-images&quot;&gt;Confirm the edition before touching images&lt;/h3&gt;
&lt;p&gt;I run &lt;code&gt;uname -m&lt;/code&gt; first, because that is the documented way to view instruction architecture. If the host is not x86_64, I treat it as an ARM deployment and stop unless a Pro license is already available. I do not infer a price, a trial, or an activation command. The deploy page states the requirement and does not document those extras, so they stay outside the decision.&lt;/p&gt;
&lt;p&gt;Image settings come next, and only after the license gate. On an ARM server the same guide says to set &lt;code&gt;ARCH_SUFFIX=-arm&lt;/code&gt; and &lt;code&gt;POSTGRES_IMAGE=chaitin/safeline-postgres-arm:15.18&lt;/code&gt;. Those values select ARM builds. They do not waive the Personal Edition limit. My written rule is simple: ARM images without a confirmed Pro license means do not install, or uninstall if a test install already happened. The documented removal path is &lt;code&gt;docker compose down&lt;/code&gt; in the SafeLine directory, then removal of that directory only when I am sure the data can go.&lt;/p&gt;
&lt;h3 id=&quot;prove-the-host-meets-the-published-minimum&quot;&gt;Prove the host meets the published minimum&lt;/h3&gt;
&lt;p&gt;Support for arm64 does not mean every small board is a safe place to put a reverse proxy. The deploy guide requires Linux, Docker 20.10.14 or above, and Docker Compose 2.0.0 or above. The minimum environment is 1 CPU core, 1 GB of memory, and 5 GB of disk. The CPU must provide ssse3. I use the listed checks: &lt;code&gt;docker version&lt;/code&gt;, &lt;code&gt;docker compose version&lt;/code&gt;, &lt;code&gt;free -h&lt;/code&gt;, &lt;code&gt;df -h&lt;/code&gt;, processor lines from &lt;code&gt;/proc/cpuinfo&lt;/code&gt;, and &lt;code&gt;lscpu | grep ssse3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I also reserve the data directory before install. SafeLine writes configuration and data there, and the guide expects at least 5 GB. The manual layout uses &lt;code&gt;/data/safeline&lt;/code&gt;. If memory, disk, Docker, or ssse3 fails, I fix the host first. Putting a filter in front of a healthy origin on an unfit host creates an outage that looks like an attack.&lt;/p&gt;
&lt;h3 id=&quot;decide-what-on-will-change-for-clients&quot;&gt;Decide what on will change for clients&lt;/h3&gt;
&lt;p&gt;SafeLine is a self-hosted web application firewall. It sits in front of an application as a reverse proxy, filters and monitors HTTP and HTTPS, and blocks malicious traffic while forwarding the rest (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The application guide adds that it is developed based on nginx: it receives traffic for the original site, cleans malicious traffic, and forwards safe traffic upstream (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Before I add an application, I write down the three required fields. Domain can be a domain name, a hostname, or an IP address. Port is what SafeLine will listen on, such as 80 or 443, with SSL checked for HTTPS applications. Upstream is the real address SafeLine will use to reach the origin. After those settings, the domain has to resolve to the SafeLine server. That DNS change is the moment protection is actually on for visitors. I do not make it until the upstream answers through the proxy.&lt;/p&gt;
&lt;p&gt;I also pick controls deliberately instead of enabling every feature on the first day. Documented capabilities include defenses against SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse. Separate controls cover IP-based rate limiting, a web access control list, CAPTCHA challenge, an authentication challenge that blocks visitors who do not enter the password, and dynamic protection that encrypts HTML and JavaScript on each visit. Rate limiting is described as throttling traffic that exceeds defined limits, including DoS attempts, brute-force attempts, and traffic surges. I enable the extra challenges only after a normal page load works. A password wall or a CAPTCHA on a broken upstream hides the real fault.&lt;/p&gt;
&lt;h3 id=&quot;choose-the-install-path-and-the-release-channel&quot;&gt;Choose the install path and the release channel&lt;/h3&gt;
&lt;p&gt;Automatic install is the recommended path. The English command in the deploy guide is &lt;code&gt;bash -c &quot;$(curl -fsSLk https://waf.chaitin.com/release/latest/manager.sh)&quot; -- --en&lt;/code&gt;, and it requires root. I still run the edition and host gates before I paste it, because a successful script is not the same as a supported Personal Edition on ARM.&lt;/p&gt;
&lt;p&gt;Manual install is the path I use when I want the ARM suffix visible before &lt;code&gt;docker compose up -d&lt;/code&gt;. That path installs Docker if needed, creates &lt;code&gt;/data/safeline&lt;/code&gt;, downloads &lt;code&gt;compose.yaml&lt;/code&gt;, writes &lt;code&gt;.env&lt;/code&gt;, generates the documented secrets, and fixes permissions. The console port is &lt;code&gt;MGT_PORT&lt;/code&gt;, 9443 unless I change it. After startup I open &lt;code&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/code&gt; and create the first administrator with &lt;code&gt;docker exec safeline-mgt resetadmin&lt;/code&gt;. I store that initial password before anyone else logs in.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RELEASE=-lts&lt;/code&gt; is a separate decision. The deploy guide says the LTS version will no longer be maintained starting from 9.1.0-LTS on 14 August 2025, and it strongly recommends upgrading to the latest version. If I am uneasy about a new release, the same page recommends upgrading about one month later. I never choose LTS as a way around the ARM Pro requirement.&lt;/p&gt;
&lt;h3 id=&quot;turn-the-proxy-on-then-test-then-cut-over&quot;&gt;Turn the proxy on, then test, then cut over&lt;/h3&gt;
&lt;p&gt;When the checks above are true, I add the application and only then move a test name to the SafeLine address. I replay the documented probes against my own domain: a SQL injection union-select, an XSS onerror payload, a path traversal toward &lt;code&gt;/etc/passwd&lt;/code&gt;, a code-injection probe, and an XXE-style payload. A blocking page means that request was stopped. I open the console attack list and read the detail view so the block is recorded, not just displayed. I also load a known-good URL. Protection is on only when bad requests are blocked, good requests still reach the origin, and I can move DNS back if the proxy path fails.&lt;/p&gt;
&lt;h4 id=&quot;does-personal-edition-work-on-arm64-&quot;&gt;Does Personal Edition work on ARM64?&lt;/h4&gt;
&lt;p&gt;No. The deploy guide says SafeLine on ARM requires a Pro license to work properly, and that the Personal Edition is not supported on ARM. &lt;code&gt;ARCH_SUFFIX=-arm&lt;/code&gt; only selects ARM images.&lt;/p&gt;
&lt;h4 id=&quot;what-host-checks-should-pass-before-installation-&quot;&gt;What host checks should pass before installation?&lt;/h4&gt;
&lt;p&gt;Linux on x86_64 or arm64, Docker 20.10.14 or newer, Docker Compose 2.0.0 or newer, at least 1 core, 1 GB of memory, 5 GB of disk, and ssse3. The deploy guide names &lt;code&gt;uname -m&lt;/code&gt;, the Docker version commands, &lt;code&gt;free -h&lt;/code&gt;, &lt;code&gt;df -h&lt;/code&gt;, CPU info, and &lt;code&gt;lscpu | grep ssse3&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;what-changes-when-protection-is-turned-on-&quot;&gt;What changes when protection is turned on?&lt;/h4&gt;
&lt;p&gt;Visitors reach SafeLine instead of the origin. SafeLine listens on the configured port, filters HTTP or HTTPS, and forwards safe traffic to the upstream. The application domain must resolve to the SafeLine server, so the cutover is a DNS and proxy change.&lt;/p&gt;
&lt;h4 id=&quot;how-can-i-test-blocking-without-inventing-new-attacks-&quot;&gt;How can I test blocking without inventing new attacks?&lt;/h4&gt;
&lt;p&gt;Use the probes in the application guide, pointed at your own domain: SQL injection, XSS, path traversal, code injection, and XXE. Expect a blocking page, then confirm the event in the console attack list detail view. Also request a normal page so you know the upstream still works.&lt;/p&gt;
&lt;h4 id=&quot;does-the-lts-channel-remove-the-arm-edition-limit-&quot;&gt;Does the LTS channel remove the ARM edition limit?&lt;/h4&gt;
&lt;p&gt;No. LTS is a release choice, set with &lt;code&gt;RELEASE=-lts&lt;/code&gt;. Maintenance for that line stops at 9.1.0-LTS, dated 14 August 2025, and the deploy guide recommends the latest version. The Pro license requirement for ARM remains either way.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33279/why-i-check-arm64-edition-limits-before-turning-safeline-on</guid>
<pubDate>Sun, 01 Nov 2026 03:46:28 +0000</pubDate>
</item>
<item>
<title>Booting ARM64 Linux in my Rust KVM Hypervisor: Getting to the console with help of eBPF</title>
<link>https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</link>
<description>&lt;h3 id=&quot;recap&quot;&gt;Recap&lt;/h3&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous article&lt;/a&gt; I got ARM64 Linux as far&lt;br&gt;
as executing inside my Rust KVM hypervisor. I still couldn't actually interact&lt;br&gt;
with the guest, though: I had no working console.&lt;/p&gt;
&lt;p&gt;This article is about getting from &quot;the PC is moving&quot; to typing &quot;hello&quot; into&lt;br&gt;
Linux and getting &quot;HELLO&quot; back.&lt;/p&gt;
&lt;h3 id=&quot;uart&quot;&gt;UART&lt;/h3&gt;
&lt;p&gt;Last time we learned how to obtain the device tree blob from QEMU's &lt;code&gt;virt&lt;/code&gt;&lt;br&gt;
machine. Now we want to take a look at what's inside. We can decompile it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;dtc -I dtb -O dts -o virt.dts virt.dtb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, a simple search through the file already gives us the clue we need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i uart virt.dts -A5 -B1
        pl011@9000000 {
                clock-names = &quot;uartclk\0apb_pclk&quot;;
                clocks = &amp;lt;0x8000 0x8000&amp;gt;;
                interrupts = &amp;lt;0x00 0x01 0x04&amp;gt;;
                reg = &amp;lt;0x00 0x9000000 0x00 0x1000&amp;gt;;
                compatible = &quot;arm,pl011\0arm,primecell&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, now I had a lead: I needed to emulate the &lt;code&gt;pl011&lt;/code&gt; UART at &lt;code&gt;0x09000000&lt;/code&gt; so&lt;br&gt;
that the existing Linux &lt;code&gt;pl011&lt;/code&gt; driver could communicate with it. This was not a&lt;br&gt;
really interesting part, as most of it was simply reading Linux kernel source&lt;br&gt;
code along with &lt;code&gt;pl011&lt;/code&gt; documentation from ARM. Soon I was able to get output&lt;br&gt;
from Linux and it felt really great:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.000000] Linux version 7.1.3 (docker@stjepan-Aspire-F5-573G) (aarch64-linux-gnu-gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP PREEMPT Thu Jul 16 08:52:24 UTC 2026
[    0.000000] KASLR disabled on command line
[    0.000000] Machine model: linux,dummy-virt
[    0.000000] efi: UEFI not found.
[    0.000000] OF: reserved mem: Reserved memory: No reserved-memory node in the DT
[    0.000000] NUMA: Faking a node at [mem 0x0000000040000000-0x0000000047ffffff]
[    0.000000] NODE_DATA(0) allocated [mem 0x47fc10c0-0x47fc37bf]
                  ### omitted further kernel log ###
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-custom-initramfs&quot;&gt;Creating custom initramfs&lt;/h3&gt;
&lt;p&gt;Now I needed bidirectional UART support so that I could send input to the guest&lt;br&gt;
as well. To make further investigation easier, I created a very simple binary&lt;br&gt;
written in &lt;code&gt;C&lt;/code&gt; to be run as the Linux init process: it just prints out current&lt;br&gt;
interrupt statistics and asks user for input, echoing it back in all caps. All&lt;br&gt;
we then need is &lt;code&gt;initramfs.txt&lt;/code&gt; file with some simple folders and devices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dir / 0755 0 0
dir /dev 0755 0 0
dir /proc 0555 0 0
dir /sys 0555 0 0
nod /dev/kmsg 0600 0 0 c 1 11
nod /dev/console 0600 0 0 c 5 1
file /init /home/stjepanp/Develop/initramfs/init 0755 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I only needed enough of a filesystem to provide &lt;code&gt;/init&lt;/code&gt;, &lt;code&gt;/dev/console&lt;/code&gt;, and&lt;br&gt;
access to &lt;code&gt;/proc&lt;/code&gt; for interrupt statistics. Then, in the Linux source code&lt;br&gt;
folder, we generate &lt;code&gt;initramfs&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;./usr/gen_initramfs.sh ../initramfs/initramfs.txt -o initramfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see at which memory address to add &lt;code&gt;initramfs&lt;/code&gt;, we can run ordinary QEMU with&lt;br&gt;
our &lt;code&gt;initramfs&lt;/code&gt; and see what happens:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;KERNEL=/home/pi/Image
qemu-system-aarch64                                             \
        -M virt,dumpdtb=virt.dtb                                \
        -enable-kvm                                             \
        -cpu host                                               \
        -kernel ${KERNEL}                                       \
        -append &quot;console=ttyAMA0 nokaslr rdinit=/init&quot;          \
        -serial stdio                                           \
        -nographic                                              \
        -nodefaults                                             \
        -initrd &quot;./initramfs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After decompiling &lt;code&gt;virt.dtb&lt;/code&gt; (explained in &lt;a rel=&quot;nofollow&quot; href=&quot;../kvm-rust-arm-linux-part1&quot;&gt;previous&lt;/a&gt; article) we can see the location:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~/dtb $ grep initrd virt-initramfs.dts -B2 -A3
        chosen {
                bootargs = &quot;console=ttyAMA0 initramfs_async=0 rdinit=/init nokaslr&quot;;
                linux,initrd-start = &amp;lt;0x0 0x40200000&amp;gt;;
                linux,initrd-end = &amp;lt;0x0 0x40294200&amp;gt;;
                stdout-path = &quot;/pl011@9000000&quot;;
                kaslr-seed = &amp;lt;0x8114b902 0xe815aa93&amp;gt;;
        };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do have to recalculate &lt;code&gt;initrd-end&lt;/code&gt; if size of our &lt;code&gt;initramfs&lt;/code&gt; file changes.&lt;br&gt;
If that happens, we can easily recompile the device tree blob after changing&lt;br&gt;
the value. Also, let's not forget to load &lt;code&gt;initramfs&lt;/code&gt; in Rust code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;const LOAD_ADDR: u64 = 0x40_000_000;
const INITRAMFS_OFFS: u64 = 0x200_000;
const KERN_OFFS: u64 = 0x200_000 + INITRAMFS_OFFS;

impl VM {
    pub fn arch_load_linux(&amp;amp;mut self, vcpu: &amp;amp;mut VCPU, args: &amp;amp;Args) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        let linux_mem_idx = self.add_mem_region(1024 * 1024 * 1024, LOAD_ADDR)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;dtb_path, 0x0)?;
        self.load_file_to_memory(linux_mem_idx, &amp;amp;initramfs_path, INITRAMFS_OFFS)?;
// ...
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setting-up-vgic&quot;&gt;Setting up vGIC&lt;/h3&gt;
&lt;p&gt;Even without having the ability to send keystrokes there was one big problem&lt;br&gt;
with the Linux console &lt;code&gt;stdout&lt;/code&gt;: The kernel messages were reaching the console,&lt;br&gt;
but once execution reached &lt;code&gt;/init&lt;/code&gt;, output stopped behaving as expected. It&lt;br&gt;
took me some time to realize that the ARM architectural timer was also involved.&lt;br&gt;
Its interrupts need to be delivered through the virtual GIC, so I first needed&lt;br&gt;
to create a vGIC. It turned out creating the vGIC was easy. I just followed the&lt;br&gt;
logic from QEMU source code:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&quot;&gt;https://elixir.bootlin.com/qemu/v11.0.1/source/accel/kvm/kvm-all.c#L4035&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In Rust this translates to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let mut create_device: kvm_create_device = unsafe { std::mem::zeroed() };
create_device.type_ = KVM_ARM_DEVICE_VGIC_V2;
create_device.fd = 0xffffffff;
create_device.flags = 0;

unsafe { libc::ioctl(self.fd, KVM_CREATE_DEVICE, &amp;amp;mut create_device); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked. Also note that I have omitted proper error handling to keep the&lt;br&gt;
code short and concise.&lt;/p&gt;
&lt;h4 id=&quot;tracing-irq-injection&quot;&gt;Tracing IRQ injection&lt;/h4&gt;
&lt;p&gt;Now if we run our &lt;code&gt;initramfs&lt;/code&gt; with &lt;code&gt;strace&lt;/code&gt; as before, and we try to type in&lt;br&gt;
some keys, we will see that in our log we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ grep -i irq kvm.log | tail -n5
458398 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7fd932dbd0) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db30) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db20) = 0
458403 ioctl(9&amp;lt;anon_inode:kvm-vm&amp;gt;, 0x4008ae61 /* KVM_IRQ_LINE */, 0x7f85c1db70) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So our &lt;code&gt;ioctl&lt;/code&gt; for IRQ injection actually takes an address as argument. If we&lt;br&gt;
investigate this, we can find out how &lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; is handled in the Linux&lt;br&gt;
kernel:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/virt/kvm/kvm_main.c#L4776&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Reading this code we see that the relevant function is &lt;code&gt;kvm_vm_ioctl_irq_line&lt;/code&gt;&lt;br&gt;
that takes &lt;code&gt;struct kvm_irq_level&lt;/code&gt; as second argument and this is what we need&lt;br&gt;
to interpret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/* for KVM_IRQ_LINE */
struct kvm_irq_level {
/*
 * ACPI gsi notion of irq.
 * For IA-64 (APIC model) IOAPIC0: irq 0-23; IOAPIC1: irq 24-47..
 * For X86 (standard AT mode) PIC0/1: irq 0-15. IOAPIC0: 0-23..
 * For ARM: See Documentation/virt/kvm/api.rst
 */
union {
__u32 irq;
__s32 status;
};
__u32 level;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this purpose, we can treat it as an 8-byte structure containing the IRQ number and level. Therefore, we can easily inspect it again via eBPF when we&lt;br&gt;
run QEMU with our &lt;code&gt;initramfs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kprobe:kvm_vm_ioctl_irq_line { printf(&quot;irq_level=%x\n&quot;, *((uint64 *)arg1)); }'
Attaching 1 probe...
irq_level=1000021
irq_level=1000021
irq_level=1000021
irq_level=1000021
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without wasting much time we can easily reconstruct this behavior in Rust:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;pub fn arch_update_irq(level: u32, vm_fd: libc::c_int) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut irq: kvm_irq_level = unsafe { std::mem::zeroed() };
    irq.level = level;
    irq.__bindgen_anon_1.irq = 0x1000021;

    let ret = unsafe {
        libc::ioctl(vm_fd, KVM_IRQ_LINE, &amp;amp;mut irq)
    };

    if ret &amp;lt; 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that, in essence, &lt;code&gt;0x1000021&lt;/code&gt; is not an arbitrary magic number. On ARM64,&lt;br&gt;
&lt;code&gt;KVM_IRQ_LINE&lt;/code&gt; encodes the interrupt type and interrupt ID in the &lt;code&gt;irq&lt;/code&gt; field. Here &lt;code&gt;0x01&lt;/code&gt; selects an SPI and &lt;code&gt;0x21&lt;/code&gt; is interrupt 33 the UART interrupt from&lt;br&gt;
the device tree.&lt;/p&gt;
&lt;h4 id=&quot;no-such-device-or-address&quot;&gt;No such device or address&lt;/h4&gt;
&lt;p&gt;However, when we run our program, it fails with &quot;No such device or address&quot;. To&lt;br&gt;
get some clue on what is going on, we can try &lt;code&gt;strace&lt;/code&gt; and get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(5, KVM_RUN, 0)                    = -1 ENXIO (No such device or address)
Error: Os { code: 6, kind: Uncategorized, message: &quot;No such device or address&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first impulse was to search for &lt;code&gt;ENXIO&lt;/code&gt; in Elixir bootlin, but,&lt;br&gt;
unfortunately, there were simply too many functions and branches that could&lt;br&gt;
return &lt;code&gt;ENXIO&lt;/code&gt;. Rather than trying to follow every possible &lt;code&gt;ENXIO&lt;/code&gt; path&lt;br&gt;
manually, I decided to use a more brute-force approach. I attached &lt;code&gt;kretprobe&lt;/code&gt;s&lt;br&gt;
to the KVM functions and looked for one returning &lt;code&gt;-ENXIO&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:kvm_* /retval == (uint32)(-6)/ { printf(&quot;%s\n&quot;, probe); }'
Attaching 487 probes...
kretprobe:kvm_vgic_map_resources
kretprobe:kvm_arch_vcpu_run_pid_change
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second function here does not seem really relevant, so I tried looking for&lt;br&gt;
&lt;code&gt;kvm_vgic_map_resources&lt;/code&gt; on Elixir:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&quot;&gt;https://elixir.bootlin.com/linux/v6.1.21/source/arch/arm64/kvm/vgic/vgic-v2.c#L289&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now taking a look at the function we can clearly see where &lt;code&gt;ENXIO&lt;/code&gt; is returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int vgic_v2_map_resources(struct kvm *kvm)
{
    struct vgic_dist *dist = &amp;amp;kvm-&amp;gt;arch.vgic;
    int ret = 0;

    if (IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_dist_base) ||
        IS_VGIC_ADDR_UNDEF(dist-&amp;gt;vgic_cpu_base)) {
        kvm_debug(&quot;Need to set vgic cpu and dist addresses first\n&quot;);
        return -ENXIO;
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have a hint. We need to set vGIC CPU and distributor addresses first. To&lt;br&gt;
confirm our hypothesis we can try tracing this function specifically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pi@raspberrypi:~ $ sudo bpftrace -e 'kretprobe:vgic_v2_map_resources { printf(&quot;ret=%u\n&quot;, retval); }'
Attaching 1 probe...
ret=4294967290
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;finding-out-distributor-address&quot;&gt;Finding out distributor address&lt;/h4&gt;
&lt;p&gt;My first attempt to set up distributor address didn't go well. I wrote the&lt;br&gt;
following code (I extracted distributor address from the device tree and&lt;br&gt;
tried to reconstruct the logic from QEMU source code):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;let dist_addr: u64 = 0x08_000_000;
let mut dev_attr: kvm_device_attr = unsafe { std::mem::zeroed() };
dev_attr.group = KVM_DEV_ARM_VGIC_GRP_ADDR;
dev_attr.attr = KVM_VGIC_V2_ADDR_TYPE_DIST;
dev_attr.flags = 0;
dev_attr.addr = dist_addr;

unsafe { libc::ioctl(create_device.fd as i32, KVM_SET_DEVICE_ATTR, &amp;amp;mut dev_attr) };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I ran the program, I got:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ioctl(6, KVM_SET_DEVICE_ATTR, 0x7ffe2696d8) = -1 EFAULT (Bad address)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To debug this, I needed to inspect the values defined in&lt;br&gt;
&lt;code&gt;struct kvm_device_attr&lt;/code&gt;, but it was a lot more complicated: I had to check if&lt;br&gt;
the &lt;code&gt;struct&lt;/code&gt; I was tracing was for &lt;code&gt;KVM_DEV_ARM_VGIC_GRP_ADDR&lt;/code&gt; group and for&lt;br&gt;
&lt;code&gt;KVM_VGIC_V2_ADDR_TYPE_DIST&lt;/code&gt;. So, I decided to write my eBPF program in C with&lt;br&gt;
and called it &lt;code&gt;probe-kvm.bpf.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;linux/bpf.h&amp;gt;
#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;bpf/bpf_helpers.h&amp;gt;
#include &amp;lt;bpf/bpf_tracing.h&amp;gt;
#include &amp;lt;linux/kvm.h&amp;gt;
#include &amp;lt;stdbool.h&amp;gt;
#include &amp;lt;stdint.h&amp;gt;

char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;

SEC(&quot;kprobe/vgic_v2_set_attr&quot;)
int BPF_KPROBE(kprobe_vgic_v2_set_attr, void *dev, struct kvm_device_attr *dev_attr)
{
        struct kvm_device_attr attr;
        __u64 value;

        bpf_probe_read_kernel(&amp;amp;attr, sizeof(attr), dev_attr);
        if (attr.group != KVM_DEV_ARM_VGIC_GRP_ADDR) {
                return 0;
        }

        if ((attr.attr != KVM_VGIC_V2_ADDR_TYPE_DIST) &amp;amp;&amp;amp; (attr.attr != KVM_VGIC_V2_ADDR_TYPE_CPU)) {
                return 0;
        }

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr);

        return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To compile and run it I wrote the following script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

clang -O2 -g -mcpu=v1 -target bpf -I /usr/include/aarch64-linux-gnu -c probe-kvm.bpf.c -D__TARGET_ARCH_arm64 -o probe-kvm.bpf.o
sudo rm -rf /sys/fs/bpf/vgic
sudo bpftool prog loadall probe-kvm.bpf.o /sys/fs/bpf/vgic autoattach
sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, I started ordinary QEMU along my eBPF trace and got this output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ ./start-probe.sh
           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139387: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fd4d3c450

           &amp;lt;...&amp;gt;-146994  [002] d..31 953951.139418: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fd4d3c450
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This really looked like a userspace address while I was supplying a raw address.&lt;br&gt;
To find out what was at that address I modified the last line of the BPF program&lt;br&gt;
above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;        if (bpf_probe_read_user(&amp;amp;value, sizeof(value), (void *)attr.addr) != 0)
                return -1;

        bpf_printk(&quot;\nset_attr: attr=%s addr=0x%llx, *addr=0x%llx\n&quot;, attr.attr == KVM_VGIC_V2_ADDR_TYPE_DIST ? &quot;DIST&quot; : &quot;CPU&quot;, attr.addr, value);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this I got what I didn't really expect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183145: bpf_trace_printk: 
set_attr: attr=CPU addr=0x7fdc806ca0, *addr=0x8010000

           &amp;lt;...&amp;gt;-147143  [000] d..31 954397.183177: bpf_trace_printk: 
set_attr: attr=DIST addr=0x7fdc806ca0, *addr=0x8000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the &lt;code&gt;addr&lt;/code&gt; field is actually a pointer to the address, and not the address&lt;br&gt;
itself. Note that &lt;code&gt;0x8010000&lt;/code&gt; and &lt;code&gt;0x8000000&lt;/code&gt; are the guest physical&lt;br&gt;
addresses of the GIC CPU interface and distributor taken from the virt machine's&lt;br&gt;
device tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;pi@raspberrypi:~/dtb $ grep -i intc virt.dts -A3
        intc@8000000 {
                phandle = &amp;lt;0x8001&amp;gt;;
                reg = &amp;lt;0x00 0x8000000 0x00 0x10000 0x00 0x8010000 0x00 0x10000&amp;gt;;
                compatible = &quot;arm,cortex-a15-gic&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also explained the &lt;code&gt;EFAULT&lt;/code&gt;: KVM was trying to dereference&lt;br&gt;
&lt;code&gt;attr.addr&lt;/code&gt; as a userspace pointer. With this small fix, I got a fully working&lt;br&gt;
Linux console:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rs&quot;&gt;dev_attr.addr = &amp;amp;dist_addr as *const u64 as u64;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following snippet shows that my &lt;code&gt;init&lt;/code&gt; process (as part of&lt;br&gt;
&lt;code&gt;initramfs&lt;/code&gt;) was run and it echoed back my input in capital letters:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[    0.832623] clk: Disabling unused clocks
[    0.834046] PM: genpd: Disabling unused power domains
[    0.845565] Freeing unused kernel memory: 12672K
[    0.847475] Run /init as init process
           CPU0       
 11:        199 GIC-0  27 Level     arch_timer
 13:          0 GIC-0  33 Level     uart-pl011
IPI0:         0       Rescheduling interrupts
IPI1:         0       Function call interrupts
IPI2:         0       CPU stop interrupts
IPI3:         0       CPU stop NMIs
IPI4:         0       Timer broadcast interrupts
IPI5:       168       IRQ work interrupts
IPI6:         0       CPU backtrace interrupts
IPI7:         0       KGDB roundup interrupts
Err:          0

type a string: hello
allcaps: HELLO

[    2.224776] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    2.227382] CPU: 0 UID: 0 PID: 1 Comm: init Not tainted 7.1.3 #1 PREEMPT 
[    2.229749] Hardware name: linux,dummy-virt (DT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interrupt statistics are also printed from my &lt;code&gt;init&lt;/code&gt;. Also, when &lt;code&gt;init&lt;/code&gt;&lt;br&gt;
exits, we get the (correct) Kernel panic that init process was terminated.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;At this point I finally had what I was looking for: a real Linux userspace&lt;br&gt;
running inside my own KVM-based hypervisor, with a working UART, interrupt&lt;br&gt;
delivery and a minimal &lt;code&gt;initramfs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;More importantly, I now had a way to interact with the guest and inspect what&lt;br&gt;
was happening inside KVM. That gives me a much better foundation for the next&lt;br&gt;
step: implementing more of the virtual hardware needed to turn this into a&lt;br&gt;
useful ARM64 virtual machine.&lt;/p&gt;
&lt;p&gt;You can also check out the repository on my GitHub page and, if you have a&lt;br&gt;
Raspberry Pi 4B, try the VMM yourself:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/StjepanPoljak/kvm-rust&quot;&gt;https://github.com/StjepanPoljak/kvm-rust&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28617/booting-arm64-linux-in-my-rust-kvm-hypervisor-getting-to-the-console-with-help-of-ebpf</guid>
<pubDate>Fri, 25 Sep 2026 14:40:06 +0000</pubDate>
</item>
</channel>
</rss>