Validate remote sequence data in sequencesync worker#64
Draft
NikolayS wants to merge 1 commit into
Draft
Conversation
11 tasks
get_and_validate_seq_info() extracted most columns of the result set returned by the publisher's sequence synchronization query with a bare Assert(!isnull). Remote responses must be validated at runtime rather than assumed well-formed: an incompatible, broken, or malicious publisher could return NULL values that the sync query normally never produces. Such a response would crash assert-enabled builds; production builds would interpret the NULL datum as zero, feeding garbage such as a zero page LSN or bogus sequence parameters into the local sequence state, or dereference a null pointer on platforms where int64 is passed by reference. In addition, the sequence index echoed back by the publisher was used with list_nth() without verifying that it identifies a unique member of the current batch. An out-of-range value would perform an out-of-bounds access, resulting in undefined behavior, while an out-of-batch or duplicate index would associate the returned state with the wrong local sequence and corrupt the accounting used to detect rows missing from the response. Fix this by replacing the assertions with runtime checks that report a protocol violation naming the affected sequence and column, and by verifying that each sequence index received falls within the current batch and appears at most once. Also declare the index column of the result set as int4, matching what the query returns, instead of narrowing an int8 with DatumGetInt32(). This follows the precedent of commit 3cf5264, which added a response-shape check for CREATE_REPLICATION_SLOT one layer down in libpqwalreceiver. The column count of the result set needs no additional check here, as libpqrcv_processTuples() already verifies it against the expected number of fields. The has_sequence_privilege and last_value columns are also left alone, since NULL is a meaningful result for those and is already handled. Sequence synchronization is new in the current development cycle, so no released branch is affected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtxA6B4d47ietk5dGsseuP
NikolayS
force-pushed
the
claude/testplan-sequencesync-null-hardening
branch
from
July 18, 2026 17:30
b45298b to
3c26436
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Defect
get_and_validate_seq_info()insrc/backend/replication/logical/sequencesync.cextracted most columns of the result set returned by the publisher's sequence-synchronization query with a bareAssert(!isnull):seqidxis_calledpage_lsnseqtypidseqstart,seqincrement,seqmin,seqmax,seqcycleRemote responses must be validated at runtime rather than assumed well-formed. An incompatible, broken, or malicious publisher returning NULL in any of these columns would crash assert-enabled builds; production builds would interpret the NULL datum as zero (feeding garbage such as a zero
page_lsnor bogus sequence parameters into local sequence state viaSetSequence()andUpdateSubscriptionRelState()), or dereference a null pointer on platforms where int64 is passed by reference.Additionally, the echoed
seqidxwas used withlist_nth()without verifying that it identifies a unique member of the current batch. A malformed response could therefore cause an out-of-bounds access, resulting in undefined behavior, associate returned state with the wrong local sequence, or corrupt thebatch_missing_countaccounting so that genuinely missing rows escape reporting.This is the same defect class as commit 3cf5264 ("Check CREATE_REPLICATION_SLOT response shape in libpqwalreceiver"), one layer up: that commit hardened the replication-command response shape; this one hardens the SQL result values consumed by the sequencesync worker.
What changed
slot_getattr_notnull()that raisesereport(ERROR, errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("invalid response from publisher"), errdetail(...))naming the affected sequence and column, following the message style of libpqwalreceiver's existing response-shape checks.Assert(!isnull)value extractions to use that helper.seqidxNULL assert to a runtime error and added batch membership + uniqueness validation:copy_sequences()passes the batch base index, batch size, and a per-batchbool *batch_seenarray (palloc0_array, batches are capped at 100) intoget_and_validate_seq_info(), which rejects any index outside[batch_base, batch_base + batch_size)and any index seen more than once in the same batch — all beforelist_nth()or anyseqinfoupdate. This is strictly stronger than a global range check: an out-of-batch or duplicate index would update the wrongLogicalRepSequenceInfoand corruptbatch_missing_count.seqidxresult column asINT4OID(matching the%dint4 VALUES column the query actually returns) instead ofINT8OIDnarrowed withDatumGetInt32(), so a malformed value is rejected by input parsing rather than silently truncated before the range check.Not changed, deliberately:
libpqrcv_processTuples()(ERRCODE_PROTOCOL_VIOLATION, "invalid query response").has_sequence_privilege) and 3 (last_value) keep their explicit, semantically meaningful NULL handling (concurrent drop / insufficient privilege, per 55f5186).Assert(col == REMOTE_SEQ_COL_COUNT)sanity check is purely local bookkeeping and stays an assert.Test evidence
Built with
meson setup --buildtype=debug -Dcassert=true -Dtap_tests=enabled; full rebuild clean, andsequencesync.ccompiles warning-free with-Werror.pgindent --diffon the file is empty.Full
setup+subscriptionmeson suites with this exact code (post batch-validation and INT4OID changes):The skip is
012_collation(ICU disabled in this build).036_sequencespassed (exit status 0, 7.08s); it exercises the modified code path end-to-end (initial sequence sync,REFRESH PUBLICATION,REFRESH SEQUENCES, mismatch/warning cases). Note the existing tests exercise only valid responses; the new error branches are untested, since injecting a malformed response would require a mock publisher:libpqrcv_connect()runsALWAYS_SECURE_SEARCH_PATH_SQL, forcing an emptysearch_pathon the publisher session, so the sync query's unqualified references resolve only inpg_catalogand cannot be shadowed from a TAP test.Closes #62
🤖 Generated with Claude Code
https://claude.ai/code/session_01PtxA6B4d47ietk5dGsseuP