Gary Bordey / POV

· Database operations · For anyone who signs off a maintenance window

The patch is not the finish line.

Installing a security fix changes the version number. Knowing the service still works requires another kind of evidence.

The maintenance ticket is closed. Its last lines read "binaries replaced, service restarted, health check passing," and someone has stamped it complete. The ticket in this dispatch is invented, but the sequence it records is routine: a security release arrives, a maintenance window is booked, the database software is replaced, the server comes back up. Everything on the ticket is true. What it does not say is whether the application that depends on the database still behaves the way it did the day before.

Last Thursday, February 12, the PostgreSQL project released updates to every supported version of the database: 18.2, 17.8, 16.12, 15.16 and 14.21. They fixed five security vulnerabilities, three of them rated 8.8 out of 10 for severity because they could let an attacker run code on the server, along with more than 65 other bugs.1 Yesterday the project announced an out-of-cycle release, scheduled for February 26, to correct regressions introduced by that update. One is that the substring() function can now fail with an "invalid byte sequence for encoding" error on non-ASCII text that comes from a database column. The other is that a standby, a replica server kept in step with the primary, may halt with an error. The project notes that the problems may not affect all users.2 As of today the corrective release has not shipped.

My argument is that a maintenance change is complete when the behavior the business depends on has been checked, not when the change is installed. Installing a fix changes the version number. Knowing the service still works takes a different kind of evidence: verified service behavior, observed in the workload that actually matters. This is not an argument for postponing security fixes. Three of February's vulnerabilities allowed code execution, and leaving them open has its own cost. Nor does every installation run into either regression. It is an argument for treating the urgency of a fix and the verification of its result as one process, rather than as a finish line followed by an afterthought.

Maintenance work order

MNT-0213 · customer database · illustrative

ChangePostgreSQL 17.7 to 17.8
ReasonSecurity release, Feb 12
WindowFri Feb 13, 22:00

1 · Installation

  • Binaries replaced
  • Service restarted
  • Health check, SELECT 1
Complete

2 · Acceptance

  • Customer search, non-ASCII names
  • Extensions: pgcrypto; ltree reindex
  • Standby replaying, lag normal
Evaluated byDate

The version number changed. That is what this half proves.

Does the application still work? This half is still blank.

Figure 1. Illustrative work order, not a record from any real system. The installation entry shows a change made; the acceptance entry shows the evidence still missing.

A fix that tightened the rules

The origin of the substring() problem is worth a moment, because it is the opposite of what most people expect. According to the project, the regression was introduced by the fix for one of the security vulnerabilities, CVE-2026-2006.2 That vulnerability was a missing check on the length of multibyte characters, the encoding that represents letters outside the basic Latin alphabet. Without the check, a user could craft queries that overran a buffer and executed code.1 The fix added validation, and the regression is an encoding error raised on text that, the project says, it should have accepted.

This is a general pattern rather than a quirk. Many security fixes work by making software stricter about what it accepts: a length is checked, a type is validated, an input that used to pass is now refused. Strictness is the point. But a rule that refuses malformed input can also refuse legitimate input under conditions its authors did not foresee, and nothing about the word "security" in a release note makes that less likely. If anything, it is a reason to look harder at the paths the fix touched.

Why a green check can miss it

Consider what the usual checks after maintenance actually test. The server process starts. It accepts connections. A monitoring probe runs a trivial query, often SELECT 1, and receives an answer. Each of these is worth knowing. None of them calls substring() on a column of customer names with accented letters, or watches a standby long enough to see it stop. A regression that lives in a particular function, data pattern or configuration is invisible to a check that touches none of them.

The substring() case shows how narrow such a failure can be. It appears on multibyte text, and, according to the project, when the value comes from a database column. A system whose data is entirely plain ASCII would see nothing. One that stores names like Müller or Nguyễn, or text in Japanese or Arabic, and slices it with substring(), could begin returning errors to its users while every infrastructure dashboard stays green. Whether the regression matters at all depends on the installation, which is exactly why the check has to come from the installation.

The February 12 announcement made the same point more quietly. It told anyone with indexes on ltree columns who does not use the libc collation provider that they must reindex those columns after upgrading.1 For those users, replacing the binaries was the installation, and the work was not finished until the indexes were rebuilt. A ticket that records only the first step records half the job.

Checks chosen for the installation

What would a better acceptance step look like? Not a universal test suite. The checks should come from what this particular installation does, and February's notices suggest three kinds that illustrate the approach.

Representative text. Run the queries the application actually runs, on data that looks like production data, including its non-ASCII names, addresses and descriptions. If the application slices or searches text, include those paths. Extensions. List the extensions the installation uses. February's security fixes touched intarray, pgcrypto and pg_trgm, and its bug fixes touched ltree.1 For each extension in use, exercise one real operation and complete any post-update step the release notes require. Replication. If the service relies on a standby, for failover or for read traffic, confirm after the update that it is still replaying changes and that its lag returns to normal, and keep watching for a while, because a halt need not happen in the first minute.

Each check needs two things written down before the maintenance window opens: what result counts as passing, and what happens if it fails. The response will differ between installations. For one it may be moving read traffic away from an affected standby; for another, applying the project's published changes ahead of the corrective release, which the February 16 notice invites those who need the fix early to consider.2 For a third it may be accepting a degraded feature for a few days. I am not prescribing a rollback procedure; that depends on the system. I am arguing that the decision should exist before it is needed, and that a named person should own the evaluation.

Who signs the second half

There is a reason the acceptance entry so often stays blank. The person who installs a database update is usually not the person who knows what the application does with the database. An administrator can confirm that the server is running and replicating; only someone who knows the application can say whether customer search still finds the right customers. A plan that leaves the second signature to the first person tends to get the checks that person can run, which are the infrastructure checks a regression like this one slips past.

The remedy is organizational rather than technical. Before the window, the administrator and an application owner agree on the checks, and the application owner, or someone they designate, judges the results. After the window, both watch for the specific symptoms the release notes and any later notices describe. February's notice is unusually helpful here, because it quotes the error messages: "invalid byte sequence for encoding" and "could not access status of transaction."2 A team that searched its logs for those two strings on the 16th would have known within minutes whether either regression had reached it, which is a better answer than a dashboard that was green all along.

ThuFeb 12

Update released

5 security fixes, 65+ bug fixes

18.2 · 17.8 · 16.12 · 15.16 · 14.21

Fri13

Sat14

Sun15

Feb 13 to 15 · no new notices

MonFeb 16

Regression notice

substring() errors on non-ASCII text from a column

a standby may halt

TueFeb 17

Today

corrective release not yet out

ThuFeb 26

Corrective release, scheduled

not yet released

18.3 · 17.9 · 16.13 · 15.17 · 14.22

Figure 2. Dates and versions from the PostgreSQL project's announcements of February 12 and 16, 2026. The February 26 release is scheduled, not released; nothing here describes what it will contain beyond the announcement.

What is known today

Evidence in a situation like this arrives in pieces, and the dates matter. Figure 2 lays them out. On February 12 there was an update to install. On February 16 there was evidence that the update had changed behavior for some workloads. The corrective release is scheduled for February 26 and, as of this writing, does not exist. An organization that installed on the 13th and closed its ticket that night had done nothing wrong. But it would have had no reason to look again on the 16th unless someone still owned the question of whether the service worked.

That is the real difference between the two entries on the work order. Installation is an event; it happens in a window and then it is over. Acceptance is a responsibility that runs until the evidence is in, and sometimes the evidence arrives after the window has closed, from the vendor, from monitoring or from a customer. A process that ends at the stamp has no place to put that news.

Objections, considered

Testing cannot cover every workload. True, and no list of checks will. These regressions shipped despite the project's own testing. The answer is not to chase a guarantee but to cover the paths whose failure the business would notice first: its most important queries, the extensions it actually uses, the replication it depends on. A short, targeted check that runs every time is worth more than an exhaustive plan that never runs.

Delay creates risk too. It does, and this objection is the one most often underweighted by people who have just been burned by a regression. Three of February's vulnerabilities allowed code execution, and every day a known flaw stays open is a day it can be used. Verification is not a reason to wait for certainty. An organization that holds a security update until the corrective release has accepted two weeks of exposure to avoid a regression that may never touch it. The better trade is usually to install promptly, with the checks prepared, a recovery option ready and monitoring pointed at the specific behaviors at risk.

That is what a staging environment is for. Staging helps, if its data and workload resemble production, which they often do not, particularly for data like customer names. It is a good place to run the acceptance checks first. It is not a substitute for running them after the change reaches the system customers use.

A version number records what was installed, not what still works.

Before the next maintenance window

  • Add one acceptance check. Choose it from what this application actually does: a real query, on realistic data, with a stated passing result.
  • Name who evaluates it. A person, not a team, who decides whether the result passes and signs the second half of the ticket.
  • Write down the response. What happens if the check fails, decided before the window opens rather than during it.
  • Keep the question open. When new information arrives after the change, as it did on February 16, someone should know it is theirs to act on.

Back to the ticket stamped complete. Nothing on it needs to be erased. "Binaries replaced, service restarted, health check passing" is a true record of an installed change, and on February 12 it was the right change to make. What the ticket needs is a second entry beneath the first, in a different hand: the queries that were run, what they returned, who looked at the result, and the date. On February 16, that entry would have been the one worth reopening.

Sources

  1. PostgreSQL Global Development Group, PostgreSQL 18.2, 17.8, 16.12, 15.16, and 14.21 Released!, February 12, 2026. Security issues CVE-2026-2003 to CVE-2026-2007, bug fixes and updating instructions.
  2. PostgreSQL Global Development Group, Out-of-cycle release scheduled for February 26, 2026, February 16, 2026.

Read next

Every essay, by date