PostgreSQL 18.4 Deep Dive — 11 CVE Patches, io_uring Async I/O (3x Faster), OAuth 2.0, UUIDv7, and Temporal Constraints
On May 14, 2026, the PostgreSQL Global Development Group released 18.4 alongside 17.10, 16.14, 15.18, and 14.23. On the surface it looks like the fourth minor update on the 18 line, but the contents make it effectively a "security major". The same day's security advisory closed 11 CVEs in one go — four of them at CVSS 8.8 (High), one allowing remote code execution via a stack buffer overflow in refint (CVE-2026-6637), and another exposing a timing channel that lets attackers recover credentials from the MD5 password comparison code (CVE-2026-6478). It's the kind of release where an operator needs to decide on the same page whether to patch now or wait until next week.
At the same time, the five structural changes PostgreSQL 18 (GA in September 2025) brought — io_uring-backed async I/O (2–3x read throughput), native OAuth 2.0 authentication in pg_hba.conf, the timestamp-ordered uuidv7() function, Virtual Generated Columns as the new default, and Temporal Constraints (WITHOUT OVERLAPS / PERIOD) — have now arrived in a stabilized form through the 18.4 patch line. This post starts with a priority matrix for all 11 CVEs, then walks through the postgresql.conf changes that most often trip up a 17 → 18 major upgrade, the pg_hba.conf patterns for connecting OAuth to Microsoft Entra ID, Okta, and Keycloak, measurements for each of the three io_method options (sync, worker, io_uring), and the 12-step verification sequence ManoIT applied to internal RDS and on-prem PostgreSQL 18 clusters.
1. Why May 14, 2026 is an Inflection Point for Database Operations
PostgreSQL 18 went GA on September 25, 2025, with 18.1 arriving in February 2026 and 18.4 on May 14, 2026. What makes 18.4 different is the convergence of three things: (a) 11 CVEs closed in a single release, (b) 60+ bug fixes from six months of post-GA stabilization landing simultaneously, and (c) 18-era features like OAuth and io_uring now being stabilized through real patch cycles.
| Date | Release / Event | Operational Meaning |
|---|---|---|
| 2025.09.25 | PostgreSQL 18.0 GA — async I/O, OAuth, uuidv7, virtual gen cols, temporal constraints | Major features arrive; only early adopters |
| 2025.11.13 | PostgreSQL 18.1, 17.7, 16.11, 15.15, 14.20, 13.23 | First minor on the 18 line — initial bug stabilization |
| 2026.02.12 | PostgreSQL 18.2, 17.8, 16.12, 15.16, 14.21 | Final patch window for the 13 line |
| 2026.05.08 | PostgreSQL 13 EOL — no further patches | 13 workloads must migrate to 14+ |
| 2026.05.14 | PostgreSQL 18.4 + 17.10 + 16.14 + 15.18 + 14.23 — 11 CVEs patched simultaneously | Security patch required across every supported track |
| 2026.05.14 | Same day: 60+ bug fixes backported | autovacuum, logical replication, partitioning, pg_dump stability |
| 2026.11 (expected) | PostgreSQL 19.0 beta expected to begin | 18 enters its long stable phase |
The two takeaways for operators: (1) the 13 line went EOL on May 8 and 18.4 arrived six days later, forcing "13 → 17 direct migration" timelines, and (2) at least four of the 11 CVEs trigger from external attack surface (an attacker only needing socket-level connection, or a low-privilege DB user). This is not a maintenance-window-of-convenience patch; it's a "do not push to the next quarter" patch.
2. The 11 CVE Priority Matrix — Which Attack Surfaces Closed
The 18.4 release notes detail seven core CVEs in the security advisory; the remaining four are memory-safety duplicates rolled into them. The four you read first are all CVSS 8.8, and among them refint's stack buffer overflow is the only RCE triggerable by a low-privilege DB user.
| CVE | CVSS | Severity | Component | Summary | Prerequisite |
|---|---|---|---|---|---|
CVE-2026-6473 |
8.8 | High | Multiple built-in functions — memory allocator | Integer underflow allocates undersized buffer → out-of-bounds write | Normal DB user with SQL execute |
CVE-2026-6475 |
8.8 | High |
pg_basebackup / pg_rewind
|
Symlink following — origin superuser overwrites client-side files | Origin superuser + backup/restore command |
CVE-2026-6477 |
8.8 | High | Server superuser code paths | Server superuser overwrites client process stack memory | Server superuser + client RTT |
CVE-2026-6637 |
8.8 | High |
refint extension |
Stack buffer overflow → arbitrary code execution + SQL injection | Low-privilege DB user + refint trigger |
CVE-2026-6478 |
5.9 | Medium | MD5 password comparison | Covert timing channel — credentials recoverable |
md5 authentication in use (scram-sha-256 safe) |
CVE-2026-6479 |
7.5 | High | SSL / GSS negotiation | Uncontrolled recursion → sustained DoS | Anyone who can connect to a PostgreSQL socket (no auth required) |
CVE-2026-6476 |
8.8 | High | ALTER SUBSCRIPTION ... REFRESH PUBLICATION |
Schema/relation names unquoted in SQL → arbitrary SQL on publisher | Subscriber owner |
2.1 CVE-2026-6637 — refint Stack Buffer Overflow (Low-Privilege RCE)
The most dangerous of the 11 is CVE-2026-6637. refint is a legacy foreign-key integrity trigger module in PostgreSQL's contrib/spi, written in the late 1990s before native foreign keys existed. It's still packaged with the distribution and some legacy schemas still use its triggers. Before 18.4, when these triggers fire they pass column names and SQL identifiers through an internal buffer that overflows the stack — leaving a "low-privilege DB user can execute arbitrary code and perform SQL injection" state. It's the only one of the 11 CVEs that turns into RCE under a regular user's privileges, so clusters with any refint footprint must patch first.
-- Check whether refint is in use anywhere in the cluster
SELECT n.nspname AS schema_name,
p.proname AS function_name,
c.relname AS table_name,
t.tgname AS trigger_name
FROM pg_trigger t
JOIN pg_proc p ON p.oid = t.tgfoid
JOIN pg_namespace n ON n.oid = p.pronamespace
JOIN pg_class c ON c.oid = t.tgrelid
WHERE p.proname IN ('check_primary_key', 'check_foreign_key')
AND NOT t.tgisinternal;
-- Any row of output means: patch to 18.4 immediately,
-- then migrate to standard foreign keys.
2.2 CVE-2026-6478 — MD5 Password Timing Channel
Second in importance: CVE-2026-6478. The server-side comparison between the client's MD5 response and the stored hash used byte-by-byte short-circuit comparison, leaving a covert timing channel that lets attackers estimate how many leading bytes match. PostgreSQL has used scram-sha-256 by default since 2017, but late migrators, clusters keeping md5 for legacy compatibility, and clusters that explicitly set password_encryption=md5 are all in scope.
-- Find users still using MD5 authentication
SELECT rolname,
CASE
WHEN rolpassword LIKE 'md5%' THEN 'md5 (vulnerable)'
WHEN rolpassword LIKE 'SCRAM-SHA-256%' THEN 'scram-sha-256 (safe)'
ELSE 'plain/unknown'
END AS auth_type
FROM pg_authid
WHERE rolcanlogin = true
ORDER BY auth_type;
-- Also check postgresql.conf and pg_hba.conf:
-- postgresql.conf: password_encryption = scram-sha-256
-- pg_hba.conf: host all all 0.0.0.0/0 scram-sha-256
2.3 CVE-2026-6479 — SSL/GSS Unbounded Recursion DoS
Third is the no-auth-required DoS in CVE-2026-6479. An attacker who can reach the PostgreSQL socket can send a specific sequence of messages during the SSL/GSS handshake; the handler then loops into unbounded recursion, exhausts stack space, kills backend processes, and in the worse case exhausts the backend slot pool — preventing legitimate users from connecting. RDS instances exposed to the internet on port 5432 with only VPC peering / IP allowlist as guardrails, or misconfigured NodePort services, are the highest-risk targets. Short-term mitigation: restrict hostssl and hostgssenc lines in pg_hba.conf to trusted CIDRs; permanent fix: patch to 18.4 / 17.10 / 16.14 / 15.18 / 14.23.
2.4 CVE-2026-6476 — ALTER SUBSCRIPTION REFRESH PUBLICATION SQL Injection
Fourth is an SQL injection in logical replication. When ALTER SUBSCRIPTION ... REFRESH PUBLICATION runs, the subscriber re-fetches the publisher's table list and interpolates schema/relation names into SQL commands without quoting. A subscriber owner who can control the publisher-side object names could execute arbitrary SQL on the publisher. 18.4 applies quote_ident() consistently when constructing those commands. Multi-tenant SaaS environments that separate publications per customer need to patch immediately.
3. PostgreSQL 18's Async I/O — Measured Differences Between io_method Options
The biggest architectural change in PostgreSQL 18 is the async I/O (AIO) subsystem. Through 17, backend processes read disk pages synchronously — a page cache miss stalled the entire backend. 18 introduces the io_method parameter so operators can choose between three dispatch strategies.
io_method |
Behavior | Prerequisite | Typical Effect (Read-Heavy) |
|---|---|---|---|
sync |
Synchronous reads, same as pre-18 | None | Baseline |
worker (default) |
Offload I/O to a dedicated worker process pool | None (all OS) | +20–30% on local SSD, +50–150% on network storage |
io_uring |
Direct use of Linux 5.1+ io_uring kernel interface |
Linux 5.1+, build with --with-liburing
|
Lower CPU overhead vs worker, +0–50% throughput depending on workload |
3.1 Step 1: io_method=worker — Safe in Almost Every Environment
The safest first step is io_method=worker. It runs on every OS, doesn't care about kernel version, and doesn't require special build flags. A dedicated worker pool issues page prefetches and the backend polls for results. The effect is largest on network storage (AWS EBS, GCP Persistent Disk, Azure Managed Disk). classmethod's RDS PostgreSQL 18 benchmark showed worker mode delivering roughly 2–3x the sequential-scan read throughput of sync. On local NVMe SSDs, where responses are already microsecond-class, the gain is closer to +20%.
# postgresql.conf — recommended baseline for io_method=worker
io_method = worker # default. enabled automatically in 18
io_workers = 3 # worker process count, default 3
# ⚠️ note: changing requires PostgreSQL restart
effective_io_concurrency = 16 # bump prefetch depth alongside AIO
maintenance_io_concurrency = 32
# Monitoring: dispatched I/O count from pg_stat_io view
# SELECT * FROM pg_stat_io WHERE backend_type = 'io worker';
3.2 Step 2: io_method=io_uring — CPU Efficiency on Linux 5.1+
Once your workload stabilizes, the next step is io_uring. Binaries built with ./configure --with-liburing (or official RHEL/Ubuntu packages) on Linux 5.1+ can enable it. io_uring places a shared ring buffer between PostgreSQL and the kernel, cutting syscall overhead. Because no worker pool is needed, CPU usage drops vs worker mode, and high-concurrency OLTP workloads can squeeze additional throughput out. But container runtimes that block io_uring syscalls via seccomp (Docker's default seccomp profile, some GKE Autopilot nodes) will fail immediately.
# 1) Check kernel version
uname -r # must be 5.1+, 6.x recommended
# 2) Verify liburing build option
psql -c "SHOW server_version;"
psql -c "SELECT name, setting FROM pg_settings WHERE name = 'io_method';"
# → 'io_uring' should appear as an allowed enum value
# 3) Update postgresql.conf
echo 'io_method = io_uring' >> /etc/postgresql/18/main/postgresql.conf
systemctl restart postgresql@18-main
# 4) Verify io_uring dispatch from pg_stat_io
psql -c "SELECT * FROM pg_stat_io WHERE backend_type = 'client backend';"
3.3 Scope and Limits of AIO
A key constraint: PostgreSQL 18 AIO is read-only. WAL writes and checkpoint dirty-page flushes still take the synchronous path. The result is (a) read-heavy analytic workloads see the largest gains from sequential and index scans, while (b) write-heavy OLTP barely moves. shared_buffers and effective_cache_size also need to be tuned for the workload — if pages get evicted immediately after prefetch, AIO can't help.
4. OAuth 2.0 Native Authentication — Direct IdP Integration in pg_hba.conf
The second major change in 18 is that OAuth 2.0 authentication is a first-class method in pg_hba.conf. Previously the choices were LDAP, RADIUS, SSPI, PAM — for OAuth you needed an external auth proxy like pgbouncer-rr-patch or aws_iam. 18 adds oauth as a method so PostgreSQL itself validates tokens against IdPs (Okta, Microsoft Entra ID, Keycloak, Auth0, Google).
4.1 pg_hba.conf Baseline Patterns
# /etc/postgresql/18/main/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
# OAuth — Keycloak realm 'manoit'
hostssl myapp all 10.0.0.0/8 oauth issuer="https://idp.manoit.co.kr/realms/manoit" scope="openid profile email" map=oauth_map
# OAuth — Microsoft Entra ID (tenant ID required, custom scope)
hostssl analytics all 10.0.0.0/8 oauth issuer="https://login.microsoftonline.com/{tenant-id}/v2.0" scope="api://{client-id}/.default" map=oauth_map
# OAuth — Okta org
hostssl reporting all 10.0.0.0/8 oauth issuer="https://manoit.okta.com/oauth2/default" scope="openid offline_access" map=oauth_map
# Keep scram-sha-256 as backward-compat (emergency access)
hostssl all admin 10.0.0.0/8 scram-sha-256
Key parameters:
-
issuer=— IdP issuer URL. OAuth is strict about issuer matching down to case and trailing slashes. -
scope=— Requested scope. Entra ID's default scope does not work; you need a custom one likeapi://{client-id}/.default. -
map=— A mapping name inpg_ident.confthat converts external identity (alice@manoit.co.kr) into a PostgreSQL role (alice).
4.2 pg_ident.conf Mapping Patterns
# /etc/postgresql/18/main/pg_ident.conf
# MAPNAME SYSTEM-USERNAME PG-USERNAME
oauth_map /^(.+)@manoit\.co\.kr$ \1
oauth_map alice@partner.com partner_alice
oauth_map admin@manoit\.co\.kr postgres # superuser mapping
oauth_map /^svc-(.+)@manoit\.co\.kr$ svc_\1 # service account pattern
4.3 Validator Module Is Required
Important: PostgreSQL 18 core ships without an OAuth validator. Core provides the protocol handler and token validation framework; actual signature verification and claim mapping happen in a separate module. Percona's pg_oidc_validator is the most widely used open-source option, while commercial distributions (EnterpriseDB, Crunchy Data) bundle their own.
# postgresql.conf — load the validator library
oauth_validator_libraries = 'pg_oidc_validator'
# pg_oidc_validator.conf (module-specific settings)
[manoit]
issuer = "https://idp.manoit.co.kr/realms/manoit"
jwks_uri = "https://idp.manoit.co.kr/realms/manoit/protocol/openid-connect/certs"
audience = "postgresql"
require_iss = true
require_aud = true
clock_skew_seconds = 60
4.4 Client Connection
# libpq 19+ (or PostgreSQL 18 client)
psql "postgres://alice@db.manoit.co.kr:5432/myapp?\
oauth_issuer=https://idp.manoit.co.kr/realms/manoit&\
oauth_client_id=postgres-client&\
sslmode=require"
# A device-code or PKCE authorization-code flow opens in the browser.
# Once issued, the token is forwarded to the PostgreSQL backend for validation.
5. UUIDv7 — Timestamp-Ordered UUIDs as a First-Class Citizen
UUIDs have become the standard for index-friendly IDs, but classical UUIDv4 is fully random — every new key dirties a different page in the B-Tree, inflating WAL and cache misses. 18 adds uuidv7() as a standard function. UUIDv7 packs Unix epoch milliseconds into the first 48 bits and random into the rest, producing UUIDs that are sorted by time.
| Strategy | Distribution | Hot Index Pages | WAL Burden | Read Cache Hit Rate |
|---|---|---|---|---|
uuidv4() |
Fully random | Spread across all pages | High (all pages dirtied) | Low |
uuidv7() |
Time-ordered | Concentrated on latest pages | Low (localized writes) | High |
bigserial |
Increasing integer | Concentrated on latest pages | Low | High |
-- Use uuidv7() as a PK default in PostgreSQL 18
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuidv7(), -- ← 18 new
customer_id BIGINT NOT NULL,
amount NUMERIC(12,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Extract the embedded timestamp from a UUIDv7
SELECT id,
uuid_extract_timestamp(id) AS embedded_ts,
created_at,
uuid_extract_timestamp(id) - created_at AS skew
FROM orders
ORDER BY id DESC
LIMIT 5;
-- A schema pattern using uuidv7() to drop a redundant 'now()' column:
-- you can extract the timestamp from id itself, so created_at can be omitted.
Caveat: uuidv7() is approximately sorted, not strictly. UUIDs issued in the same millisecond are differentiated only by their random suffix, so high-throughput workloads still see some page fragmentation. Still, index cache hit rates typically improve by 10–30 percentage points over UUIDv4.
6. Virtual Generated Columns — Computed Columns as the New Default
PostgreSQL 12 introduced stored generated columns that materialize values on disk. 18 adds virtual generated columns and makes virtual the default. Virtual columns compute at query time, so they don't take disk space, and changing the expression doesn't trigger a table rewrite.
-- In 18, GENERATED ALWAYS AS ... VIRTUAL is the default
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT uuidv7(),
subtotal NUMERIC(12,2) NOT NULL,
tax_rate NUMERIC(5,4) NOT NULL DEFAULT 0.10,
-- VIRTUAL is implicit, keyword optional
total NUMERIC(12,2) GENERATED ALWAYS AS (subtotal * (1 + tax_rate)) STORED, -- ← STORED explicit
total_v NUMERIC(12,2) GENERATED ALWAYS AS (subtotal * (1 + tax_rate)) -- ← VIRTUAL default
);
-- STORED vs VIRTUAL: only STORED can be indexed today
CREATE INDEX idx_invoices_total ON invoices(total);
-- VIRTUAL: not indexable in 18 (under review for 19)
-- If you need an index, declare STORED explicitly.
7. Temporal Constraints — WITHOUT OVERLAPS and PERIOD
PostgreSQL 18 brings SQL-standard temporal constraints for data that has a time dimension: hotel reservations, employee tenure periods, contract validity windows.
7.1 WITHOUT OVERLAPS — Period Non-Overlap
-- Hotel reservation: bookings for the same room must not overlap in time
CREATE TABLE reservations (
room_id INT,
period daterange NOT NULL,
guest_name TEXT,
-- 18 new: WITHOUT OVERLAPS on the period column
PRIMARY KEY (room_id, period WITHOUT OVERLAPS)
);
-- Overlapping attempt — previously required a custom trigger
INSERT INTO reservations VALUES (101, daterange('2026-06-01','2026-06-05'), 'Alice');
INSERT INTO reservations VALUES (101, daterange('2026-06-03','2026-06-07'), 'Bob');
-- ERROR: conflicting key value violates exclusion constraint "reservations_pkey"
7.2 PERIOD — Temporal Foreign Key
-- Employee tenure: each employee's dept_id must reference a valid department
-- whose period contains the employee period.
CREATE TABLE departments (
dept_id INT,
period daterange NOT NULL,
dept_name TEXT,
PRIMARY KEY (dept_id, period WITHOUT OVERLAPS)
);
CREATE TABLE employee_history (
emp_id INT,
period daterange NOT NULL,
dept_id INT NOT NULL,
PRIMARY KEY (emp_id, period WITHOUT OVERLAPS),
-- 18 new: temporal foreign key using PERIOD
FOREIGN KEY (dept_id, PERIOD period) REFERENCES departments (dept_id, PERIOD period)
);
-- ⚠️ note: temporal FKs do not yet support RESTRICT / CASCADE /
-- SET NULL / SET DEFAULT on ON DELETE or ON UPDATE — only NO ACTION
Implementation detail: temporal constraints use GiST indexes internally, so they're larger than B-Tree indexes. And because ON DELETE/UPDATE actions are limited, treat temporal FK enforcement as "partially restricted relative to the SQL standard" when adopting them.
8. ManoIT Internal Cluster Verification Checklist
The 12-step sequence ManoIT applied to internal RDS PostgreSQL 18 (18.1 → 18.4) plus on-prem 18 clusters, alongside the io_method transition and OAuth rollout:
| Step | Target | Verification Command / Action | Expected Outcome |
|---|---|---|---|
| 1 | refint triggers | Run the SQL from §2.1 | 0 rows expected; if any, migrate immediately |
| 2 | MD5 users | Run the SQL from §2.2 | All should be scram-sha-256 |
| 3 | SSL/GSS exposure | Review hostssl / hostgssenc CIDRs in pg_hba.conf
|
No internet-wide (0.0.0.0/0) rules |
| 4 | Logical replication owner | SELECT subname, subowner::regrole FROM pg_subscription; |
Confirm subscriber owners are known roles |
| 5 | Apply 18.4 patch | RDS: in-place minor upgrade to 18.4 during maintenance window; on-prem: apt install postgresql-18=18.4-1
|
Confirm version 18.4, all extensions compatible |
| 6 |
io_method transition |
Keep worker or switch to io_uring (kernel 5.1+) |
Increased dispatch counts in pg_stat_io
|
| 7 | OAuth pg_hba.conf | Apply §4.1–4.3 and pg_reload_conf()
|
Keep scram-sha-256 line for emergency access |
| 8 | Adopt uuidv7() | Change new tables' PK to DEFAULT uuidv7(); existing tables go dual-column |
Watch index cache hit rate |
| 9 | Virtual Generated Columns | Only keep STORED where indexes are required | Confirm table size decrease |
| 10 | Temporal Constraints PoC | Adopt WITHOUT OVERLAPS in reservation/contract domains |
Unit-test rejecting overlapping INSERTs |
| 11 | Standby replication | Patch streaming replicas to 18.4, monitor lag | Lag returns to <1s |
| 12 | Rollback plan | 18.4 → 18.3 downgrade is unsupported → verify base-backup restore plan | Confirm 30-day PITR window |
9. Closing — The New Defaults 18.4 Sets
PostgreSQL 18.4 is a release where "major security patches and the future authentication / identification / temporal model both arrive in stable form". Among the 11 CVEs, the refint RCE (CVE-2026-6637), the SSL/GSS DoS (CVE-2026-6479), and the logical-replication SQL injection (CVE-2026-6476) demand patching now. The major-18 features — io_method=worker (default) → io_uring (Linux 5.1+), native OAuth 2.0, uuidv7(), Virtual Generated Columns, Temporal Constraints — are now the starting point for 2026 H2 new schemas.
ManoIT's recommended operational sequence: (1) apply the security patches across every supported track (14.23, 15.18, 16.14, 17.10, 18.4) within seven days; (2) audit and remove the three risk patterns — refint, MD5 authentication, unrestricted SSL/GSS exposure; (3) keep io_method=worker as a baseline and switch to io_uring on Linux 5.1+ workloads; (4) design new services starting with OAuth 2.0 authentication + uuidv7() + Virtual Generated Columns; (5) introduce WITHOUT OVERLAPS and PERIOD for reservation, contract, and history tables where the time dimension matters. The database is no longer "an engine that runs SQL" — it's now an enterprise security control point that standardizes authentication, identification, the time dimension, and the I/O model together.
This post was co-authored by Anthropic Claude (Opus 4.6) and the ManoIT engineering team. PostgreSQL 18.4 release notes and security advisories from postgresql.org are primary sources. ManoIT internal verification results are provided as reference and should not be generalized. Please credit the source when citing or republishing.
Originally published at ManoIT Tech Blog.
Top comments (0)