Vivian Voss

Your Edited Config Survives the Upgrade on FreeBSD

like for like freebsd debian upgrades

The morning after a quiet night of unattended upgrades, the log on a Debian server can carry one line that is easy to miss: Package … has conffile prompt and needs to be upgraded manually. At some point somebody changed a line in that package's configuration file. The package therefore sat out the night's upgrade, whatever the upgrade was for, and it will go on sitting out every night until a person logs in and answers a question.

The question has six possible answers, and none of them is the one an administrator of forty machines would choose.

The habit

dpkg's behaviour is written down in Debian Policy, and it rests on a sound principle. For every configuration file a package ships, dpkg records an MD5 digest. On upgrade it holds that digest against two files, the one on disk and the one in the new package. If only the package changed the file, the new version goes in. If only you changed it, yours stays. If both changed, the policy says it in one sentence: "the user is prompted about the problem and must resolve the differences themselves."

The prompt offers Y or I to install the package's version, N or O to keep yours, D to look at the differences and Z to start a shell and investigate (the brave option). The default is to keep what you have. What it never offers is to combine the two, and it cannot: dpkg keeps only a digest of the file it shipped last time, so there is no common ancestor to merge from.

Automation can pick a side in advance. --force-confold keeps your file, so the new default never arrives, and --force-confnew installs the new file over your change. A run with no terminal and neither option simply stops, with end of file on stdin at conffile prompt.

unattended-upgrades, which applies security updates on Debian and Ubuntu without anyone present, checks for this case before it starts. Its README says it "holds back any package that requires them", and the source is precise about what that means. I read the current release this morning: a function called conffile_prompt compares the same three MD5 digests dpkg would, and when all three differ the package is set aside, a warning goes to the log, the package is pinned so that nothing installs it, and the run counts as unsuccessful. Security updates get no exemption. The only way past it is to add --force-confold to every run, after which the package upgrades and your configuration never hears of the new default.

Debian does have a three-way merge, in a helper called ucf. A package has to opt into it, it looks after files that the package's own installation scripts manage, and even there the merge is one item in a menu whose default is to keep the current file. In Debian 13, 132 of 37,478 source packages depend on ucf at all.

RPM does not ask. A file marked %config(noreplace), which Fedora's packaging guidelines recommend for configuration, stays exactly as you left it, and the new version is written beside it as .rpmnew, "for reference", in the words of the RPM manual. A file marked plain %config goes the other way: yours is renamed to .rpmsave and the package's version takes its place. Nothing is merged, for the same reason as on Debian: RPM records only a digest of the shipped file. Fedora offers rpmconf for the rigmarole afterwards: it finds the .rpmnew and .rpmsave files and opens a two-way diff, and it is not installed by default.

Both habits come from the same respectable rule, that a package manager must not guess what an administrator meant. Asking is honest, and so is leaving the new file beside the old one. The cost appears at scale, where the honest question becomes forty questions, or one unanswered question that keeps a package unpatched for as long as nobody reads the log.

You edited it, the package changed it: the upgrade Debian, dpkg RHEL, RPM FreeBSD keeps an MD5 digest of the file keeps a digest of the file keeps the file as last shipped at the upgrade asks: Y, I, N, O, D or Z? default: keep yours, the new default is lost at the upgrade writes .rpmnew beside it or renames yours .rpmsave and says nothing at the upgrade merges both, three ways asks nothing unattended package held back, security fixes included unattended new values wait in .rpmnew until somebody looks unattended merged, upgrade completes merge: ucf, if a package opts in merge: rpmconf, two-way, by hand conflict only on the same lines: .pkgnew, or etcupdate resolve

The same job on FreeBSD

FreeBSD answers with one idea, implemented three times: keep a copy of the file as it was last shipped, so that there is something to merge from.

The oldest of the three is etcupdate. John Baldwin wrote it between 2010 and 2013 at Hudson River Trading, a trading firm, and the comment at the top of the script states the brief: it is "specifically tailored to assisting with mass upgrades. To that end it does not require user intervention while running." It arrived in FreeBSD 10.0. It keeps the files of the previous and the current release in two trees under /var/db/etcupdate and merges the difference between them into your copy with diff3; in the jail I keep for measurements, the current tree holds 388 files. Where your change and the release's change touch the same lines, it writes the file with conflict markers into a separate tree, leaves your live file alone, and refuses to start another merge until you have run etcupdate resolve.

freebsd-update, which upgrades systems installed from distribution sets, does the same with diff3 for everything under /etc when it moves to a new release. Its temperament is different: it shows you every merged file as a diff and asks whether it looks reasonable, and where a merge failed it opens an editor.

On systems installed from packages, which FreeBSD's scripted installer has set up by default since 15.1 and every cloud image since 15.0, the job falls to pkg itself. Since version 1.4 in December 2014 pkg has carried a three-way merge, borrowed from the Fossil version control system, for every file a package marks @config, and it keeps the shipped version of each such file in its own database, which is precisely the ancestor dpkg and RPM do without. In FreeBSD 15.1 the base system marks 374 files in 65 packages that way, sshd_config, master.passwd, login.conf, crontab and every script in /etc/rc.d among them. rc.conf, fstab, pf.conf and jail.conf belong to no package, so no upgrade touches them at all. The setting behind the merge, AUTOMERGE, is on unless you switch it off.

Software from the ports tree follows an older and plainer convention. A port installs its default configuration with a .sample suffix and copies it to the real name only when nothing is there yet. An upgrade replaces the sample and leaves your file alone, so there is nothing to ask and nothing to hold back, and the new default sits beside yours whenever you care to compare.

What FreeBSD keeps so that it can merge 374 files marked @config in 65 base packages, 15.1 388 files etcupdate keeps to merge against 2014 pkg 1.4 brings the three-way merge 10.0 etcupdate arrives in FreeBSD Counted on 13 October 2026: base_release_1 manifests for FreeBSD:15:amd64, and /var/db/etcupdate/current in a 15.0 measurement jail

The difference, shown

I built three versions of a small test package in the measurement jail this morning, each shipping one configuration file, and installed the first:

# shipped with 1.0
listen = 127.0.0.1
workers = 4
timeout = 30

Then I changed the address to 192.0.2.10, the kind of edit every server receives on its first day, and upgraded to version 2.0, which ships a new header and timeout = 60. pkg asked nothing and printed nothing about the file, and this is what it left:

# shipped with 2.0
listen = 192.0.2.10
workers = 4
timeout = 60

Both changes arrived, mine and the package's, and no file with an odd suffix appeared beside it.

Next I set timeout = 90, and version 3.0 shipped timeout = 120, the same line changed on both sides. This time pkg said Unable to merge configuration file, left my file exactly as it was, and put its own version beside it as demo.conf.pkgnew. That is the one case that needs a person, and pkg leaves it to one.

One merge, measured: pkg and a test package in three versions ancestor: 1.0 as shipped listen 127.0.0.1 · timeout 30 yours listen 192.0.2.10 · timeout 30 new: 2.0 listen 127.0.0.1 · timeout 60 three-way merge both against the ancestor merged, no question listen 192.0.2.10 · timeout 60 same line changed twice 3.0: timeout 120, yours: 90 yours stays, 3.0 goes to .pkgnew

By the rules quoted above, the same three versions would have gone differently on the other two systems. On Debian, the upgrade to 2.0 stops at the prompt, because both sides changed the file; the default keeps my address and loses the new timeout, and under unattended-upgrades the package is not upgraded at all. On RHEL with %config(noreplace), my file keeps timeout = 30 and the new value waits in demo.conf.rpmnew until somebody goes looking for it.

The limit

A merge is textual. diff3 knows lines, not meaning, and two changes on different lines can still contradict each other. A merged file deserves the glance that a merged pull request gets.

A failed pkg merge leaves the decision to you. pkg keeps your file untouched and writes its own version beside it as .pkgnew; with MERGETOOL set, it hands the three-way merge to a tool of your choice.

freebsd-update asks by design during a release upgrade. It shows each merge for approval, and for patch-level updates it does not merge at all; it leaves locally modified files as they are and lists them.

Ports are not merged. The new defaults of third-party software wait in the .sample file beside yours, which spares you both the prompt and the rename.

Linux has answers of its own. NixOS generates /etc from a single declaration and has no conflicts of this kind to resolve. etckeeper keeps /etc in git and commits before and after every apt run. Configuration management owns the file outright and rewrites it on every pass. ucf merges for the packages that opt in.

etcupdate was written at a trading firm, where a server waiting for an answer is a server earning nothing. Thirteen years on, it still does not ask.