Attackers switched supermarket refrigeration controllers across Europe into defrost mode. Fixes for known flaws had been out since 2025. What that means for firmware teams, and how to keep every release rebuildable.
In the night from 1 to 2 November, fridges and freezers in supermarkets across Europe started warming up. Stores in Belgium, Luxembourg, the Netherlands, Sweden, Finland, Estonia, Ireland and Italy reported warming cabinets or spoiled food. A store in Sweden reported cabinet temperatures of up to 50 °C. A refrigeration service company in Belgium said the systems had been switched remotely into defrost mode.
It was the second wave in a month. On 12 October, hundreds of supermarkets in France had been hit the same way. The EU Commission has since set up a task force with ENISA and Europol.
If you write firmware for connected equipment, this one is worth reading closely. Not because of how the attackers got in, which is still being investigated, but because of what the reporting says about updates.
What is known, and what is not
- Remote controllers. CERT-FR reported numerous compromises of remote controllers for refrigeration installations and listed Copeland xweb, Danfoss AK-SC, Danfoss AK-SM and Dixell xweb. Its first advice: take them off the public internet.
- Known flaws, fixed in 2025. For one of those controller families, the AK-SM 8xxA series, CISA advisory ICSA-25-140-03 describes an authentication bypass based on passwords generated from the date and time (CVE-2025-41450), fixed in R4.2, and two further flaws fixed in 4.3.1. Claroty Team82 described the "code of the day" mechanism and counted 2,765 of these systems reachable from the internet.
- Old firmware. Danfoss said, as reported by The Register, that the affected French stores "were running outdated firmware". A security adviser quoted by CNBC said that thousands of units never received the update and that the oldest models cannot be updated at all.
- Not confirmed. Nobody has claimed the attacks. Authorities have not named the entry point or confirmed that the known flaws were used.
CERT-FR's alert includes a warning every firmware team should have on the wall: an end-of-life device without security patches that stays exposed to the internet carries a very high risk of compromise.
Two reasons a fix never reaches a device
The first is on the operator side. The controller sits on a public IP so a contractor can reach it, nobody in the store owns its updates, and the patch that exists is never installed. That is what CERT-FR's advice addresses, and it is not what this post is about.
The second is on the manufacturer side, and it is the part firmware teams control. A controller sold eight years ago only gets a security update if someone can still build its firmware: with the toolchain it was built with, the SDK it was built against and the build script that lived on one engineer's laptop. Then you want to show that the new image differs from the one in the field only by the fix.
Can you rebuild the firmware you shipped years ago and get the same bytes? Not "something that boots". The same file, with the same hash as the image on the devices in the field. For most embedded teams the honest answer is "probably not". That is rarely carelessness. An embedded build depends on far more than the source tree, and almost none of it is written down.
What the CRA asks for, in plain terms
The main obligations of the EU Cyber Resilience Act apply from 11 December 2027. For products with digital elements, it expects manufacturers to handle vulnerabilities and provide security updates over a support period that typically runs for years, and to document the software that goes into each product.
The regulation does not use the word "reproducible". But if you have to patch a version that is five years old, you need to be able to build it again, and you want to know that the only difference in the new image is your patch. That is a reproducibility problem.
Whether and how the CRA applies to a specific product is for the manufacturer to decide. This post is about the engineering side.
Why the same source gives different binaries
A typical bare-metal Makefile calls arm-none-eabi-gcc and trusts whatever is on PATH. Three things leak into the output:
- Toolchain drift. One engineer has GCC 15.2 from the distro, CI has 13.x from a container image, another engineer uses the vendor's bundled toolchain. Different code generation, different binaries, same commit.
- Timestamps.
__DATE__and__TIME__, archive member times, random build IDs. Every build looks new. - Paths. Debug info (
-g3),__FILE__and some linker outputs carry absolute paths. Build in/home/anna/fwand in/builds/ci/fwand you get two images.
None of this matters on the day you ship. It matters on the day you have to ship a fix for an old version.
Three ways a rebuild fails
- The toolchain is gone. The compiler version the image was built with is not installed anywhere anymore, nobody wrote it down, and the vendor download page has moved twice.
- The build is not deterministic. You have the toolchain and the source, but timestamps and paths change the output, so you cannot show that the rebuild matches what shipped.
- The build knowledge is gone. The Makefile lived on a laptop, the SDK headers sat in a directory nobody committed, and what is left is a folder of
.cand.hfiles.
Each needs a different fix.
Sealing a toolchain: detect, seal, build, verify
The well-known answer to the first two is Nix: pin every input by content hash, build in a sandbox, set SOURCE_DATE_EPOCH, use a fixed build prefix. It works very well. It also means writing Nix expressions, which most firmware teams have no reason to learn.
So we put Nix underneath and kept it out of sight. The flow has four steps:
Here it is on a public project, step 1 of cpq/bare-metal-programming-guide at commit 594fe3c: an STM32 Cortex-M4 blinky with main.c, link.ld and a Makefile that calls arm-none-eabi-gcc. Nothing in the repo was changed.
Detect reads the build system, not the source, because that is where the signal is:
$ firmwerk detect . Selected: make (medium confidence) Build plan build system : make toolchain : arm-none-eabi-gcc target arch : cortex-m4 build : make Seal and build copies the sources into a sandbox, supplies a toolchain pinned by nixpkgs commit and content hash, runs the project's own make, and writes firmwerk.lock:
$ firmwerk lock --backend nix . Sealed make (make build) with the `nix` backend toolchain : gcc-arm-embedded [79bf431783c2] artifact : firmware sha256:sha256-y37o+ schema = 1 provider = "make" build_system = "make" build_commands = ["make"] [toolchain] id = "gcc-arm-embedded" source = "b134951a4c9f3c995fd7be05f3243f8ecd65d798" # nixpkgs commit nixpkgs_hash = "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=" hash = "79bf431783c2667c326344d3e13a347bc354cbde29a0490cf1178016501e3282" [[artifacts]] name = "firmware" sha256 = "sha256-y37o+KexPFWmv/mTn2XE7YaSU/fpXq122hTSTBP/Jck=" (Excerpt of the lock file.)
The lock is the file you commit next to the firmware. It records the exact toolchain, the build commands and the expected hash of every artifact.
Verify builds again and compares:
$ firmwerk verify --backend nix . Reproducible: all 1 artifact(s) rebuilt byte-identical. firmware [sha256-y37o+KexP] (make build) |- toolchain gcc-arm-embedded [pinned b134951a4c9f3c995fd7be05f3243f8ecd65d798] That worked despite -g3 debug info, because the sandbox uses a fixed build prefix and SOURCE_DATE_EPOCH. With the toolchain cached, the lock took about 13 seconds and the verify under two. If a build leaks a timestamp or a path into the image, verify fails and names the artifact, which is exactly what you want to know.
The test we like most: step 2 of the same guide. Built natively on a laptop with Fedora's GCC 15.2.0, firmware.bin hashes to 39c2f7c815d0…; sealed, it is sha256-FUE/MaOsdUw9…. Then we put a deliberately broken arm-none-eabi-gcc first on PATH:
$ printf '#!/bin/sh\necho "shim arm-none-eabi-gcc: refusing to run" >&2\nexit 1\n' > shim/arm-none-eabi-gcc $ PATH="shim:$PATH" make shim arm-none-eabi-gcc: refusing to run make: *** [Makefile:17: firmware.elf] Error 1 $ PATH="shim:$PATH" firmwerk verify --backend nix . Reproducible: all 1 artifact(s) rebuilt byte-identical. The native build breaks. The sealed build does not notice, because it never touches the host toolchain. Two machines with different compilers produce different images natively and the same image this way. That is the property you want eight years later, when the laptop is gone and the patch is due.
Wrapping instead of replacing
Firmwerk does not translate your build into something else. It handles Make, CMake, Zephyr's west, ESP-IDF, PlatformIO, Arduino and Rust firmware (embassy, RTIC, bare metal), and runs your existing build inside the sealed environment. For Make it follows include directives and CROSS_COMPILE prefixes. When a project's Makefile reaches into ../../libs, it seals the whole repository and builds in the subdirectory.
Developers keep building the way they do. CI runs verify on top. Nobody has to agree on a new build system before the builds become reproducible.
Recovering a lost build from bare .c/.h files
The third failure is the hard one, and it is the usual state of firmware for a model that left the catalogue years ago. Here the approach is closer to archaeology:
- Run
clang -fsyntax-onlyin a loop. Each missing header is stubbed and the pass runs again, so you walk the real include graph,#ifbranches included. - Map well-known headers to libraries:
stm32f4xx.handcore_cm4.hto STM32 HAL/CMSIS,FreeRTOS.hto FreeRTOS,lwip/*,mbedtls/*. - Take the architecture from CMSIS core headers or the vendor part family.
- Collect macros that are used in
#ifbut never defined (-Wundef) as candidate-Dflags. The holes in the source show you the old command line.
We tried it on step 5 of the same guide, keeping only main.c, hal.h and syscalls.c. hal.h includes stm32f429xx.h, which is not there:
$ firmwerk detect . Selected: source-archaeology (low confidence) Evidence - 3 source/header files, no build system <- repo scan - include closure via clang (3 passes) <- clang -fsyntax-only - library = STM32 HAL/CMSIS <- include closure Gaps ! MCU/architecture not identified from source; confirm target ! build not synthesized (architecture unknown); confirm the target to enable compilation ! libraries identified; set FIRMWERK_CACHE and run `firmwerk fetch` to pin: STM32 HAL/CMSIS ! library versions not recoverable from source; reproducibility applies from now on It does not guess the MCU, because the header that would tell it is the missing one. It names the library instead and asks you to confirm the target. firmwerk fetch pulls a pinned SDK into a local cache, the closure runs again, and the next layer shows up. For FreeRTOS that is the project's own FreeRTOSConfig.h and portmacro.h. Once the target is confirmed, Firmwerk synthesises the compile, and with startup code and a known MCU it generates a linker script and links a full ELF.
Note the last gap. Reconstruction gets you a reproducible build from today. It cannot tell you which SDK version was used in 2019.
What to take from the fridge attacks
A reproducible build does not stop anyone from logging into a controller. It decides whether you can still ship the fix when they do. Four things are worth checking on the next connected product, before it leaves the building:
- No service logins derived from something an attacker can compute. A password generated from the date is a password everyone has.
- Management interfaces are not meant for the public internet. Make the safe setup the default one, and say so in the installation guide.
- An update path that still works on the oldest hardware you support. Whether a device can still be updated at all is decided years earlier, at design time.
- Every release rebuildable. Seal the toolchain at release time and keep the lock next to the image. "Reproducible from now on" and "identical to the image we shipped in 2019" are different claims. The first one becomes routine. The second needs the original toolchain version, which is exactly the information that tends to get lost. So the best time to seal a build is the release you are about to ship.
We built this as Firmwerk, part of the BreakPilot platform. Docs: Firmwerk Docs
If you build connected controllers and want to find out whether your oldest supported release still builds, write to kontakt@breakpilot.com. We are also at SPS in Nuremberg from 24 to 26 November, hall 6, stand 6-253.3.
Have you had to rebuild an old firmware release, or patch a device that was never meant to be patched? We would like to hear how it went in the comments.







