DEV Community

daniel jeong
daniel jeong

Posted on • Originally published at manoit.co.kr

PostgreSQL 18.4 Deep Dive — 11 CVE Patches, io_uring Async I/O (3x Faster), OAuth 2.0, UUIDv7, and Temporal Constraints

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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';"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 like api://{client-id}/.default.
  • map= — A mapping name in pg_ident.conf that 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)