English version of the Russian original published on Habr.
From each according to his ability, to each according to his needs. Karl Marx, “Critique of the Gotha Programme”
After the previous article about Klipper the most frequent objection was not “you are wrong”. The most frequent one was: “So what? Fork it, patch it, use it.” And it came from people who live exactly like that, and who, by the look of it, are not bothered at all that their work will drown among millions of forks and never be of use to anyone.
So what, indeed? To answer that, we first have to agree on what “open” even means.
Public is not the same as open
Open the OSI Open Source Definition: ten criteria. Open the FSF’s four freedoms. All fourteen points are about what is guaranteed to you: to read, to change, to distribute. Not one is about the project’s obligation to accept your patch. Both canons describe the artifact and say nothing about the process.
Some people understand this and say so honestly. SQLite’s license page says it in so many words: Open-Source, not Open-Contribution. The code is open, patches are not accepted, thank you, goodbye. Valetudo from the previous article is the same honesty in other words. The contract is declared, no questions.
But in everyday usage “open source” glues two different things together. The first is the right to read and fork, and the license guarantees it. The second is the ability to bring something back, and the license does not guarantee it at all. The first is the code being public. The second is the code being open. Eric Raymond described exactly this difference back in The Cathedral and the Bazaar: a cathedral with published blueprints does not become a bazaar.
Public code is not open code. Code has to be open both for reading and for writing. Otherwise patches dissolve into endless forks, unlabeled, incompatible, never verified by anyone, and die one by one at the first rebase. This is exactly what “fork it and use it” proposes as the norm.
So far this is abstract. Let’s look at what one “just a patch” really looks like.
What “just a patch” looks like
Right now I am bringing the Keenetic KN-1012 router into OpenWrt (here and below also the Netcraze NC-1012 after the rebranding, one device regardless of the badge).
I took it on because the hardware deserves it: sturdy, modern, inexpensive, with an SFP port. The provider’s module goes into the SFP cage and the router replaces the operator’s GPON terminal. There is a huge number of GPON users who need this, in Russia, Georgia, Armenia and many other places. The drivers are mostly well understood, and I honestly counted on quick work.
The problem turned out to be not the drivers but how to glue them together. The router has an unusual topology: a combo SFP/Ethernet port, and switching between the two has to happen already in U-Boot by producing a DTS for the chosen mode. That is no longer “add a device tree by example”, that is work on three levels at once.
But the main thing is not even the complexity. The main thing is that behind the words “bring up the board” there was not one task but seven. The board itself comes up on its own, as far as that is possible at all. The fact that the kernel’s network subsystem had never seen the hardware behind the switch is a separate kernel problem that has nothing to do with the board. MAC address collisions on Keenetics in general: separate. EEE on the switch: separate. A four-year-old typo in the SPI bus width: separate. Each of these tracks deserves a life of its own: it fixes something not just for my device and therefore goes upstream as its own PR or series. There is no common umbrella for them and there cannot be. But a truly working device appears only when all of them come together.
If you are into hardware porn, the spoiler is for you. If not, it is enough to remember the number: seven.
How "an evening's port" became a kernel patchset
The KN-1012 port did not start from zero. By August I already had my first port, the Keenetic Buddy 6, #23727, four devices on one dtsi (KN-3411, KN-4410 and their Netcraze twins), and my own HeYangTek SPI-NAND driver accepted into linux-mtd. On those I had worked out all the mechanics: the zyimage firmware format, TFTP recovery without a console, NMBM. So I was not jumping into the unknown; I had a sober estimate of what I was in for, because I had walked this road once already. Buddy 6, by the way, is still waiting for review since June, but that is a subject of the first article.
Hence the estimate at the start, August 9: “same mt7981, same NMBM, same TFTP recovery, the only real unknown is which 2.5G PHY sits on the combo port and how the SFP cage is wired. It will become clear whether this is a port for an evening or for a month.” It became clear. A month and then some.
Topology. Two hours over the vendor DTS and the picture is clear: GMAC0 goes to an MT7531 switch, and the fifth switch port is a single SerDes that goes through a mux on GPIO 27 either into the SFP cage or into an Airoha EN8811H and on to the copper 2.5G RJ45. It cannot be split statically; the SerDes is shared. Precedents in the tree: the TP-Link FR365 with the same MT7981+MT7531 tandem, and the Turris Omnia, whose mainline DTS says so in plain words: “until kernel supports this configuration properly, U-Boot has to enable the sfp node”. The bootloader picks the medium on every boot, because phylink/DSA have no runtime switching. Hence a chainloaded U-Boot: the vendor’s bootm loads my U-Boot instead of the kernel, which reads mod-def0 and picks the config from a dual FIT, copper or SFP.
The predecessor fork. The same day I found a branch by @Linaro, who had started the KN-1012 port back in February. The DTS, the LEDs, the image recipe were all taken from there, with attribution. Plus two kernel patches, one of them 370 lines, which swapped phylink on a live DSA port by polling mod-def0, in a place where the framework guarantees nothing. The last commits on the branch were called try to fix sfp/lan4 and test lan4. The final state: SFP only, the copper phy-handle commented out. And the NAND model in his DTS matched the neighboring board byte for byte and did not match the real chip: copy-paste. That is the very “random fork with half the work done” from the middle of this article. The only difference is that I knew what it was missing, and only because the author himself explained it in chat.
The chainloader. The first firmware: a reboot loop, no console. I soldered the UART twice: with a fire poker instead of a soldering iron and with resin I milked from a pine tree instead of rosin (this is not a joke), with a Flipper instead of a terminal. No activity on any pin, and I did not go for a third attempt: with that poker you are more likely to finish off the board than to solder it. I debugged by LED: 68-byte “beacons” in ARM64 blinking a GPIO. It turned out the vendor’s bootm requires an ARM64 Linux Image header. Then that minimizing the defconfig had dropped CONFIG_TIMER: get_timer() does not advance, and every “spin until timeout” loop is infinite. One root for three “different” hangs: NAND reads, switch polling over MDIO, tftpboot.
Copper. The plain image booted, but lan4 did not appear: DSA binds the PHY at switch probe, two seconds after start, and the EN8811H driver is a module and loads later. The port gets validated with the generic PHY, which cannot do 2500base-T: -EINVAL, port marked unused. Building the driver into the kernel does not help: it loads firmware in probe, and /lib/firmware is not mounted yet. In the whole OpenWrt tree the EN8811H sits on a DSA switch port nowhere; everyone keeps it on the GMAC, where binding is lazy. Nobody had done this. Six versions of the DSA patch in one morning, including the one after which the board did not come back and I pulled it out through recovery with the button. The sixth, attaching the PHY on ifup when the module is already in memory, worked. The link did not come up until I intervened: the polarity of GPIO 27, derived analytically from four sources, turned out inverted on the hardware.
Then copper died. Link up, no traffic, broken frames. Stock firmware showed the hardware alive, so the bug was mine. The fix, airoha,pnswap-rx, swapped RX polarity on the SerDes pair, I tried and “disproved” in twelve seconds. While the first link on this PHY takes 55 seconds to come up. Archaeology through build logs found that the only image that had pushed 938 Mbit/s was not clean: it had that very pnswap in it. One line in the DTS, iperf 2.34 Gbit/s, three reboots out of three. That is what “the patch applied, but the functionality is not guaranteed, and checking takes expertise” means.
The kernel. The DSA patch “attach the PHY on ifup” went to netdev as a series of three, with two fixes found along the way. Andrew Lunn replied the next day: “this is not guaranteed to work… think about the case of NFS root”. The same evening: “moving the binding of MAC to PHY into open is wrong… I need to think on it for a while”. And a day later he sent the whole architecture and asked me to find holes in it: before the firmware is loaded the chip is not a PHY but a microcontroller in a bootloader, and it has to be described as a separate MDIO device that loads the firmware and publishes the PHY on a nested bus, while phylink gets a slow-to-probe property and waits. From his “have a think” to my “I built it” took forty hours: two implementation variants, a test matrix, PHY attach at 7.0358 seconds in every boot with a spread of 47 microseconds, which is the poller tick, not a race. Two more emails, and on August 29 an RFC of nine patches went out. My original approach did not go into the kernel, and rightly so: it broke NFS root. In OpenWrt it lives temporarily as a local carry, until the architectural variant reaches mainline.
The two fixes found along the way became a separate series in net. First: phylink remembers the PHY pointer before the last step that can fail, and on failure a second disconnect detaches the PHY again and drops references already dropped. Second: a PHY that went through a binding cycle with the generic driver stays in polling forever, and the real driver never sees the interrupt from the device tree. Paolo Abeni asked for Fixes tags; archaeology led to the commit that gave birth to phylib: both halves of the bug were there from day one. Lunn put his Reviewed-by on the first patch.
The third series is EEE. The mt7530 and mtk_eth_soc drivers fill lpi_capabilities but not lpi_interfaces, and phylink considers a MAC EEE-capable only with both. Result: EEE has been off on every port of every mt753x and every mtk_eth_soc MAC since the drivers were converted, and cannot be enabled from userspace; phylink takes the other branch and calls phy_disable_eee. Daniel Golle caught a factual error about 2.5G in the justification, and Paolo asked to limit the fix to confirmed SoC revisions. Confirming was on me: MMD register 3.1 bit 11, Tx LPI indication, read by a 30-line utility over SIOCGMIIREG, because the board has neither devmem nor python. Thirty threads of three hundred packets each with LPI armed: 9000 out of 9000.
What in the end is not about my board. The first series is for any chip that needs firmware to become a PHY. The second is for any PHY with a modular driver on an unmounted rootfs. The third is for all MediaTek switches and SoC MACs at once. The board merely exposed them.
In OpenWrt this is two boards, six devices counting every badge, and five separate problems: Buddy 6 in #23727, KN-1012 in #24820, #24623 about MAC address generation, #24819 with phylink/phylib backports and the carried DSA patch, #24863 with the EEE backports, #24809 with the SPI bus width typo. The typo deserves its own paragraph: it is four years and four months old. It arrived in the tree in April 2022 with the mt7986 bring-up, from a MediaTek engineer, out of the vendor reference DTS, twelve copies at once. Every later board honestly copy-pasted it from the reference, and all these years the devices quietly lived on a one-bit SPI bus instead of a four-bit one. Nobody noticed, because “slow but works” does not look like breakage. Copy-pasting the reference is the most efficient delivery vector for a typo into a tree, and one fix upstream closes it on every board at once: on the Teltonika RUTC50, reading 64 MiB sped up from 13.5 to 5.7 seconds. Next to it, a patch in firmware-utils: the fork of the zyimage utility had lost a byte order conversion, and thirty devices in the tree are tuned to the wrong order. Plus a patch in U-Boot with support for the FM25G01B and FM25G02B SPI-NAND, because the chip in my unit was known to Linux but not to U-Boot. The netdev series: the RFC of nine patches, phylink and phylib, EEE. There were a few more, old designs and rejected versions, which we do not count.
This is painful, interesting and useful. And I could have simply stopped at my own fork, as people advise me to.
What the one who “just forks” gets
What would the person who finds that fork get? Look at the list above. It is not one patch. It is seven independent tracks, and here is what is wrong with them: without their neighbors, each of them sort of works. The main PR, and the device boots. Without the MAC generation fix it works until a virtual interface gets the same address as a physical one and traffic starts going to the wrong place. Without the PHY fixes it works until the link on the combo port starts flapping. Without the DSA backport it works, but not the way it should. And the SPI bus width typo shows itself in nothing at all, except that flash access is four times slower than it could be.
Now imagine someone who finds my fork a year from now. Most likely they will find the main PR; it is the most visible one. They will build the firmware. They will get a device where half of what this was all about does not work, and they will not understand why. I admit honestly: my main PR has cross-links to the other six tracks. But they are there because it was in my interest to raise visibility, and they are addressed to reviewers, not to someone hunting for firmware a year later. That is my choice, not the norm. In the general case there are no links: one person brings up the board, another fixes a bug in OpenWrt, a third fixes a kernel subsystem, and they know neither about each other nor that their work is connected. There is nowhere to mark seven tracks across three projects as one set, except a personal blog or a recipe on 4PDA. The tracks meet only in mainline. And that is also where it turns out which problems were avoided; more precisely, it does not turn out: they simply converge in a release, and nobody ever learns that without one of them half the devices would not work. This is what a fork cannot give in principle.
Suppose our person did find the recipe and assembled the set by hand. Next come conflicts between patches written at different times against different branches, plus three series in the kernel and a U-Boot patch from other repositories. But conflicts are not the most expensive part. A patch that applied cleanly to a kernel two, three, four versions after it was written is not obliged to work: git checks text, not behavior. The subsystem under it may have changed semantics, and the patch will quietly do the wrong thing. So after every rebase, another couple of hours of tests. And that is provided there is the expertise to understand what exactly to test: I “disproved” a 55-second link in twelve seconds, and I had been sitting in this hardware for a month. Even if I ran all of this as one branch with no intention of reaching mainline, every release would cost me hours of rebasing and several test iterations. For an outsider, many times more.
That is what “fork it and use it” means in practice: you use it. Alone. Until you get tired of rebasing. The kernel does not get the DSA bug fixes. OpenWrt does not get the device. People with a provider terminal get nothing. Days of my work, to exaggerate, do not make the world better; they make my home network better, and that is all.
“To each according to his needs” without “from each according to his ability”
But for the forker himself it is still rational, isn’t it? He got his device in an evening instead of a month of correspondence. And he is right, for himself. A patch in an evening is cheaper than a week of review where you get asked about design, about style, about tests and about why you did it this way and not that way. Locally everything adds up.
The mistake is not in the individual calculation but in the horizon. A patch in a fork costs not an evening but an evening multiplied by the number of releases you will have to live through, because a rebase is not free and conflicts accumulate. A patch upstream costs a week of review once, and after that other hands rebase it, fix it and carry it to new branches. Usually by the second or third release the fork is more expensive. And if what you are writing has already been written by somebody and sits in a random fork, you pay twice: for reinventing it and for dragging it along afterwards. Review in mainline hurts, but it is a one-time pain. And that pain is not a ritual, it is a signal. If your code is really good, bug-free and made by the subsystem’s rules, review goes fast and barely touches you. It hurts exactly where the code is bad, and in a fork you will learn about that too, only later and at a higher price: not from a reviewer in an email but from hardware that hung, lost data or let out the magic smoke. Review is not an entry tax, it is an extra guard that a fork simply does not have. A fork is a subscription.
Don’t take my word for it, take OpenWrt’s. It is a huge project with hundreds of contributors, and towards the kernel it is itself a downstream. And OpenWrt does not like carrying patches. In the general case, when you bring them a kernel fix, they ask you to at least try to take it upstream. This is not a polite request; it is the structure of the repository. In target/linux/generic there are three directories: backport, already accepted into the kernel and backported; pending, sent to the kernel and waiting; hack, what cannot be taken upstream, kept minimal. For your patch to get into pending, it has to have been sent up. On every kernel bump, patches that reached the stable branch get thrown out; the commits say so: “removed, upstreamed”. A project with more hands than any of us did the math and decided: carrying patches is too expensive, even for it. That is exactly why my phylink and EEE fixes sit in OpenWrt as backports and pending, and not as yet another layer of patches that would have to be rebased forever.
If OpenWrt cannot afford it, a loner with a fork cannot either.
But there is a level above the personal. An upstream contribution is a public good: one pays, everyone uses. A fork is a private good: one pays, one uses. Everyone rationally picks the private one, and the sum is a scattering of incompatible, unverified patches from which nobody can retrieve anything. It is the tragedy of the commons in reverse: nobody overgrazes the common pasture, nobody sows on it either.
This is where the epigraph comes back. “Fork it and use it” is half of the formula. To each according to his needs: yes, take it, the license allows. From each according to his ability: no, why, I am fine in my fork. This is not communism. It is half of it, and exactly the half that does not work without the other.
Except that “public good” is a poor motivation for a hacker with a soldering iron. He is not a party functionary.
What is in it for you personally?
More than it seems. And I want to say this as someone who went to the Linux kernel with patches for the first time this year.
I broke a pile of rules. Not on purpose; they just do things differently from everywhere else, and the etiquette is not readable at first glance. I wrote replies in threads that were too long. I broke the netdev twenty-four-hour rule, do not send a new version of a series sooner than a day later. And so on down the list. Nobody sent me to read the documentation. I was picked up, guided, and people spent, really spent, hours of their time so that the design of my patches became right. Not “accept as is” but “this is how it should be done, and here is why”. The best example is the fate of my DSA patch. It worked: the board came up, the port lived. Andrew Lunn, the maintainer of the PHY subsystem, replied that the approach was wrong in principle, it broke NFS root, and wrote “I need to think”. Two days later he sent a complete architecture for the solution and asked me to find holes in it. He did not brush me off; he designed for me what I could not design myself, because I did not know the subsystem the way he does. Forty hours later I replied “I built it”, and four days after that an RFC of nine patches by his design went to netdev. That is not review. That is school. Senior review of this level in commercial development costs a lot, and here it is given for free to anyone who brought working code and is ready to listen. By the way, on his remarks the commit messages got rewritten from version to version: “leave only the why in the message, the diff will tell the mechanics”.
Second, free maintenance. After the merge your code stops being your problem. When the subsystem is refactored, whoever refactors it fixes your driver. When the API changes, they port it. The NAND driver I brought into the kernel in June will live there longer than I will remember it exists, and it will not be me who fixes it.
Third, reputation, and it is not abstract. That same SPI bus width typo fix touched twelve boards, and I do not have most of them. I asked in the OpenWrt chat for help with testing, and the next day Routerich, a vendor whose routers were on the list, contacted me and offered to say thanks with hardware: two routers. Not for supporting its devices, I had never held them in my hands, but for a fix to a shared typo that had hit them too. Reputation works the other way as well, when you slip. Recently I broke a rule in the Gateway API repository. The maintainer did not turn me away or send me to read CONTRIBUTING; he wrote: “do it the same way you do it in Cilium”. I am active in both projects, and by the time of the mistake I already had a reputation. The mistake was forgiven not because it was small but because a history of useful contributions stood behind it. People who come to my personal projects with PRs and issues more and more often write something like “thanks for your work on Cozystack, I follow you”. Upstream makes you visible, and visibility brings you both contributors and vendors. Reputation in open source is currency, and it is earned only on the public field. In a fork nobody sees you.
And fourth, the least noticeable: your hardware will outlive your interest in it. A fork dies when you get bored. Upstream does not.
All of this works on one condition: the other side plays too, the project responds to what is brought to it.
And if the project does not respond?
Everything said above is the duties of the one who brings. Now about the one who accepts. Because this game is played by two, and if the second side does not play, the first side loses its meaning too.
“Nobody owes anybody anything” is the eternal argument, and for SQLite it is true: they promised nothing and take nothing. But if a project collects stars, takes sponsor money and invites people to contribute, it has already received its half of the deal. Responding to what comes in, proposals, issues, PRs, is the other half. Not altruism, an obligation under the deal. What exactly to take, what fits the mission, what most people need or what the maintainer simply likes, is his right. Staying silent for years is not.
In March 2024 I opened a PR to Cloudflare’s official Helm chart, cloudflare/helm-charts#69, an option to disable the default 404. Two and a half years. Zero replies from the maintainers. And not because the repository is dead: in September 2024 they merged someone else’s #74; they came in, saw the queue and walked past. After that, two years without a single substantive commit. Meanwhile people with the same pain gathered in my PR thread, and the last comment there is a stranger posting a link to my fork as the solution. That is how I became the maintainer of my own chart, which people install instead of the official one. A second PR to the same organization, cloudflared#1582, hot reload of the config, since January: zero comments. Two PRs, two silences: that is no longer an accident, that is a way of running a project.
And here is what a project that responds looks like. Obico has no Helm expertise, and I did not want to keep the chart at my place; the Cloudflare scenario appeals to me from no side. In June I opened an issue proposing an official chart, three weeks later brought a PR, and it was accepted. Then a signed OCI artifact, then arm64 images, then startup probes. I asked to be made a maintainer and in my hobby time I develop the chart at their place, not mine. Nobody has a fork. Twenty-three days from idea to accepted chart, against two and a half years of silence. The difference is not the size of the company. The difference is whether the project responds.
Note: I do not demand acceptance. I demand a response. A refusal with reasons is a normal outcome. Silence is not. How projects can avoid drowning in the stream of PRs and which ones to take is a separate conversation, and it will happen. But reacting to input is part of the contract if you called yourself open.
And if you don’t want to, say so, like SQLite. That is honest, and then everyone knows what to do: fork without illusions. A fork by itself is not evil.
When a fork is fine
A fork is a legitimate tool, and the cases where it is appropriate are more numerous than you might think after everything I wrote above. One thing unites them: a fork is a conscious choice where getting to mainline is noticeably more expensive than carrying the maintenance burden yourself. The key word is conscious: the price of the burden is counted in advance, not discovered after the fact.
When you are tinkering with a pet project in the garage, take someone else’s code and quickly throw a patch on it to close your own pain. That is fun, and you owe nobody anything.
When you are big and you need something only you need. That is acceptable: you consciously put a liability on the balance sheet and you know its price.
When the delta is architectural, not a feature. In the previous article I talked about blockstor, a LINSTOR-compatible control plane rewritten the Kubernetes-native way. That is not a patch, that is a different project, and you do not bring such things in through a PR.
When the project is not glad to see you. It said so honestly, like SQLite; it stays silent, like Cloudflare; or it simply does not want your patches for political or any other reasons. Then a fork is the only way out, and you go into it without illusions: it is not “my code will arrive later”, it is “my code lives here”.
And the most obvious case, the one people forget: the fork as a staging area. Without a fork you cannot even send a PR; that is how GitHub works. A branch in a fork where the patch lives while review and tests are going on is not a fork in the sense of this article. It is transport to mainline.
In none of these cases is the fork itself the problem. The problem is when a big open project stays silent and the community lives on forks not by choice but because there is nowhere else to go. Then the fork is not a tool but a symptom. That is what the previous article was about.
Playing as two
So the formula is simple. The community brings patches to projects. Projects help those patches get finished and accepted. Neither side works without the other: without an incoming stream the project is a cathedral, without a reaction to the stream it is a cathedral with published blueprints.
So if there is a patch lying in your drawer that closed your pain, bring it. Yes, they will ask about the design. Yes, you will have to redo it. That is the school, and it is free. In return, other hands will maintain your code, your hardware will outlive your interest, and your work will not dissolve in a fork nobody will ever hear of.
To each according to his needs the license already guarantees. From each according to his ability is on you.