Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wgVDK-006E1v-2q for pgsql-hackers@arkaria.postgresql.org; Sun, 05 Jul 2026 22:21:27 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wgVDH-00E08E-1c for pgsql-hackers@arkaria.postgresql.org; Sun, 05 Jul 2026 22:21:23 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wgVDH-00E086-0a for pgsql-hackers@lists.postgresql.org; Sun, 05 Jul 2026 22:21:23 +0000 Received: from mail-pj1-x1033.google.com ([2607:f8b0:4864:20::1033]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wgVDE-00000001war-40Ks for pgsql-hackers@postgresql.org; Sun, 05 Jul 2026 22:21:22 +0000 Received: by mail-pj1-x1033.google.com with SMTP id 98e67ed59e1d1-3811f512167so2028764a91.3 for ; Sun, 05 Jul 2026 15:21:20 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=leadboat.com; s=google; t=1783290078; x=1783894878; darn=postgresql.org; h=user-agent:in-reply-to:content-disposition:mime-version:references :message-id:subject:cc:to:from:date:from:to:cc:subject:date :message-id:reply-to; bh=ghMdDYLyFqIzyasURgEpR2tER+Sh8p3i3IPc08HHjeY=; b=LAvLcwccbphhzyz/k7CR+e9IVLj1muyZCHxBmgEkFdOpq5kdKz4s52UYMhCXrPETCP ailD8Nwd1R1rbvwtAiKH/F86yPMy6K6N3WBkHonfsYVf2JKr3YZMswlJnov0Sif2uVV1 XL6EPmlhPFIEY/2gwzNJu6hn2+sN4isGs4kag= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783290078; x=1783894878; h=user-agent:in-reply-to:content-disposition:mime-version:references :message-id:subject:cc:to:from:date:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=ghMdDYLyFqIzyasURgEpR2tER+Sh8p3i3IPc08HHjeY=; b=j7XM8mB67Yt2jZJyVCh0zbI0puWcQZ27IYQq0ysdRQYHRrEjDgT+Sb5co8EmQIZc/U ausMNAKoVUlJyXmaqQ5b1xvUkVFG8pT54Se3UJL7aO6KrMlEwq+DPdlEBN8G2SU1nPHT NPMgpmyqGImwfkyIRH3DyRJFNwD6aPP4DYp+r+/icZ7jPYNdbVuMABmO0iU19EFytgYg UxSx3k3Y7fiVvEzFsJbt3KgE6TnjEKyc3Pw2ve27CU2GbnTCMK1qOTarlOBO4PGgoSxB lE5nPJ7OCe6pdAIcAMXy6/236jL7ZoGN+x7cnLtij6t2gks0Gc2Pp6HK7f7w/cGVT1K9 q/6Q== X-Forwarded-Encrypted: i=1; AHgh+RqTE0BJXKGacsAvVo7dgm4tuxWDhQWC+LBrDC4ofdEl3WuKiuMY0QBfHCi524wBQ2nOVpsWGBSe0OKCiKcH@postgresql.org X-Gm-Message-State: AOJu0YzXwnvAVRgMG8Hvax80LMZIexx/4Ek2x7FV4EwDodAGdj5OZ7Hr zXCuEOBc5oRlLOoHf2IGB1y4jrwsTINjhSdYm3mC2AxyH1r88DkY6uczaRW+00ltxw== X-Gm-Gg: AfdE7cnFOTdDzl6mF2GjCn2Hd0QDACLESZxUv7Ewif2hp1gpttJcJnx/sUcE5ai1VUy 3tiegr6yYgk65uH4rK7cv3OdMWnvv9Dm3yWJgWkXnH6PFkUBMPcZc55TsYdWFiOZO1X4z3iVp/3 DnhEBQJaSC0p59PgajacZd8UKpIVGeyuAf7/QTky9UMfa26poBY/O2F5pC5Wf6BOei5tDXwV4et GikPFgC0kve3N6fZJnzjf5p1fKxcQIofpaIMKg1vXJEozkZGeidDoMbV3ostTHWlmbN0LF7F9pw 1fXZwq2BOcnmj4O2kp2G6fwEepjxW95MkDEsSt2NT3SUXMak56ilyi1UPZNZb8EIaiypaMlDasu r5/wOd6lZM66rE5jkUKc3KiaqN65Ix6y3sXI8q7re0ddLuFz2amwqmtmb5z1U0SYu0ON3AZzGQb xGkYKGQL42wAB3tvEYe4GNI15z/bah06fzG7+493RAXxibBEjhPyINA38= X-Received: by 2002:a17:90a:d40f:b0:381:fa5:521f with SMTP id 98e67ed59e1d1-382806b14d8mr7271333a91.3.1783290078515; Sun, 05 Jul 2026 15:21:18 -0700 (PDT) Received: from microsoft.com (c-73-15-160-255.hsd1.ca.comcast.net. [73.15.160.255]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-30f39e07e0bsm28368394eec.30.2026.07.05.15.21.17 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Sun, 05 Jul 2026 15:21:17 -0700 (PDT) Date: Sun, 5 Jul 2026 15:21:15 -0700 From: Noah Misch To: Amit Langote Cc: Junwang Zhao , Ayush Tiwari , Nikolay Samokhvalov , pgsql-hackers mailing list , Andrey Borodin , Kirk Wolak Subject: Re: PG19 FK fast path: OOB write and missed FK checks during batched Message-ID: <20260705222115.be.noahmisch@microsoft.com> References: MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline In-Reply-To: User-Agent: Mutt/2.3.0 (2026-01-25) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Fri, Jun 12, 2026 at 11:46:07AM +0900, Amit Langote wrote: > I've pushed these now. Thank you everyone. commit 4113873 wrote: > Confine RI fast-path batching to the top transaction level I discourage this fix strategy, for three reasons: 1. It slows "BEGIN; SAVEPOINT s; COPY table_with_fk FROM ..." by the ~1.6x batching benefit, compared to omitting the SAVEPOINT. That's a bad user experience not seen elsewhere. Starting a high number of subtransactions is expensive, but wrapping a long-running transaction body in one subtransaction hasn't been a performance reducer. 2. It departs from the PostgreSQL norm of tracking resources by subtransaction. You can see normal handling in many AbortSubTransaction() callees, e.g. AtEOSubXact_LargeObject(). This in turn makes the change harder to verify as correct. 3. It doesn't seem to have simplified code much, compared to our normal subxact-based approach. > First, on subtransaction abort ri_FastPathSubXactCallback discarded the > entire cache. An entry's batch holds rows buffered by the enclosing > transaction, not just the aborting subxact -- the cache is keyed by > constraint, so a single entry can mix rows from multiple subxact levels. That would imply having started a subtransaction and then added to the batch without an intervening pair of CommandCounterIncrement() and AfterTriggerBeginQuery(). That's an invalid thing for C code to do, so I'd make it an error if a batch would contain rows from different subtransactions. (This relates to the reentrancy bug fixed in 0e47bb5. With an intervening CommandCounterIncrement() and AfterTriggerBeginQuery(), reusing the outer batch is wrong even without subtransactions: it would check with the wrong snapshot. Each level of reentrancy needs to finish its FK checks separately, even if the batch buffer were unbounded.) > An internal subxact abort during after-trigger firing (e.g. a PL/pgSQL > BEGIN ... EXCEPTION block) therefore dropped buffered rows I suspect a subxact abort during after-trigger firing can still cause a different problem via AfterTriggerEndQuery(): AfterTriggerEndQuery(EState *estate) { ... afterTriggers.firing_depth++; AfterTriggerEndSubXact() doesn't undo this increment. Having identified that as suspect, I asked Opus 4.8 to try to confirm or refute bug reachability. I have not personally verified its finding, but it said: CLAUDE [CONFIRMED -- reachable bug; your instinct is right]: firing_depth is ++/-- in matched pairs inside AfterTriggerEndQuery/FireDeferred/SetState with NO PG_TRY, and AfterTriggerEndSubXact resets firing_batch_callbacks but NOT firing_depth. So a trigger ERROR caught by an outer subtransaction (PL/pgSQL EXCEPTION) skips the -- and strands firing_depth>0 for the rest of the xact. Its sole consumer is AfterTriggerIsActive() -> the RI_FKey_check batching gate; the only RI check that runs OUTSIDE genuine trigger firing is ALTER TABLE / VALIDATE CONSTRAINT per-row validation. With firing_depth stranded, that validation is wrongly routed into ri_FastPathBatchAdd. Repro (per-row forced via REFERENCES-only, no SELECT, so RI_Initial_Check bails): CONTROL: per-row-validated ALTER ADD FK with violating row 99 -> errors AT the ALTER (correct). BUG: run a caught FK violation first (DO/EXCEPTION), then the same ALTER in the same xact -> the ALTER does NOT error, marks convalidated=t, emits "WARNING: resource was not closed: relation pk2 / pk2_pkey / TupleDesc" (a resource-owner leak), and defers the violation to COMMIT. Consequences: resource leak + FK validation deferred past the ALTER (documented invariant broken). Fix: reset firing_depth in AfterTriggerEndSubXact, mirroring the firing_batch_callbacks reset right below it. Even if that's a hallucination, it's an example of what I meant in (2) about making the change harder to verify. Also, it's not clear to me why AfterTriggerEndSubXact() is right to reset afterTriggers.firing_batch_callbacks. The outer xact may be firing. If that's okay, can you expand the code comment to explain it? > Cleanly unwinding the cache on subxact abort would require tracking the > originating subxact of each buffered row, since rows from different > levels share an entry (the cache is keyed by constraint) and deferred > constraints cannot be flushed early at a subxact boundary. It's true that they can't be flushed early, but I'm not seeing a need for explicit code to avoid that. A subxact commit shall just confirm there's no batch of its subxact level. A subxact abort shall discard any batch of its subxact level, leaving higher-subxact batches untouched. A deferred trigger doesn't start a batch until the end of the top-level transaction. > The per-row fast path still bypasses SPI and stays well ahead of the > pre-19 SPI-based check. A fuller fix that preserves batching across > subtransactions -- whether by tracking the originating subxact of each > buffered row or by per-subxact cache stacks merged into the parent on > commit -- is left for a future release. If the above suspicion corresponds to a live bug, I'd bet on the fuller fix being cleaner than a surgical fix. That may not pan out, but I recommend trying it first.