# v0.58.0 — Security & Correctness Hardening (Full Details) > **Summary:** [v0.58.0.md](v0.58.0.md) > **Assessment source:** [plans/PLAN_OVERALL_ASSESSMENT_12.md](../plans/PLAN_OVERALL_ASSESSMENT_12.md) > **Findings addressed:** S-1, S-2, S-3, S-4, C-1, C-2, C-3, C-4 This release addresses all HIGH-severity findings from the v0.57.0 overall assessment (Report 12). No new SQL API surface is added. Every change is a targeted security fix, correctness fix, or correctness-observable improvement to an existing code path. --- ## Security Fixes ### SEC-1: attach_outbox() / detach_outbox() / attach_embedding_outbox() Ownership Check **Assessment finding:** S-1 (HIGH) **Files:** `src/api/outbox.rs` — `attach_outbox_impl`, `detach_outbox_impl`, `attach_embedding_outbox_impl` The three outbox management functions are missing the `check_stream_table_ownership()` call that all other mutating APIs (`alter_stream_table`, `drop_stream_table`, `pause_stream_table`, `resume_stream_table`) use as a hard gate. **Before:** Any role with `EXECUTE` on the `pgtrickle` schema could attach a `pg_tide` outbox to a stream table owned by a different role and thereby observe or modify its change stream. **After:** `check_stream_table_ownership()` is called immediately after `StreamTableMeta::get_by_name()` in all three functions. A non-owner receives `PgTrickleError::NotStreamTableOwner` with a descriptive message. ### SEC-2: stream_table_to_publication() / drop_stream_table_publication() Ownership Check **Assessment finding:** S-2 (HIGH) **File:** `src/api/publication.rs` — `stream_table_to_publication_impl`, `drop_stream_table_publication_impl` Same gap as SEC-1 but for the logical-replication publication API. A non-owner could issue `CREATE PUBLICATION` over another user's stream table, exposing its storage to arbitrary subscribers. **After:** Same fix as SEC-1 applied to both publication functions. ### SEC-3: DDL Hook Escalates Instead of Silently Returning on SPI Error **Assessment finding:** S-3 (MEDIUM) **File:** `src/hooks.rs` — `handle_alter_table` When `find_view_downstream_pgt_ids()` encounters an SPI error (e.g., during catalog contention or a concurrent DROP EXTENSION), the handler now: 1. Retries the query once with a fresh `Spi::connect()`. 2. If the retry also fails, raises `pgrx::error!()` with the message `"pg_trickle: DDL hook could not inspect dependencies — schema change blocked to prevent inconsistent state"`. This ensures that an upstream ALTER TABLE that would invalidate stream tables does not proceed silently when catalog inspection fails. ### SEC-4: Schema Identifier Quoted in CDC Buffer Name Construction **Assessment finding:** S-4 (MEDIUM) **File:** `src/cdc.rs` — `buffer_qualified_name_for_oid` **Before:** ```rust format!("{change_schema}.{base}") ``` **After:** ```rust sql_builder::qualified(change_schema, &base) ``` The `change_schema` value is now properly quoted using the centralised `sql_builder` API, preventing malformed SQL if an unusual schema name (spaces, mixed case, special characters) is used. --- ## Correctness Fixes ### COR-1: Multi-Column NOT IN Rewrite — NULL Row Safety **Assessment finding:** C-1 (HIGH) **File:** `src/dvm/parser/sublinks.rs` — multi-column IN/NOT IN rewrite path The v0.55.0 multi-column IN rewrite converts `(a, b) IN (SELECT x, y …)` to a SemiJoin and `NOT IN` to an AntiJoin. For `NOT IN`, SQL mandates that any `NULL` value on either side of the correlation predicate produces UNKNOWN, which means the outer row must be excluded. AntiJoin semantics differ: a correlation predicate that evaluates to UNKNOWN keeps the row. **Fix:** Before generating the AntiJoin rewrite for `NOT IN`, the parser now: 1. Inspects whether any element of the left-side row constructor is a `NULL` constant expression (`IS_NULL_CONST`). 2. Inspects whether any column in the subquery's target list is nullable (non-strict expression or explicitly `NULL`). 3. If either condition is detected, the rewrite is skipped and the original subquery-based execution path is used instead, with a diagnostic `NOTICE`-level message: ``` NOTICE: pg_trickle: multi-column NOT IN with nullable elements cannot be rewritten to an anti-join; falling back to subquery-based delta computation. ``` `docs/LIMITATIONS.md` gains a new subsection documenting this behaviour. ### COR-2: Recursive CTE Depth Guard Applied in DIFFERENTIAL Mode **Assessment finding:** C-2 (HIGH) **File:** `src/dvm/operators/recursive_cte.rs` — strategy selection **Before:** ```rust let max_depth = if matches!(ctx.delta_source, DeltaSource::TransitionTable { .. }) { crate::config::pg_trickle_ivm_recursive_max_depth() } else { None // no guard in DIFFERENTIAL mode }; ``` **After:** ```rust let max_depth = crate::config::pg_trickle_ivm_recursive_max_depth(); ``` The guard is now unconditional and consistent across both modes. When `max_depth` is reached in DIFFERENTIAL mode, the recursive CTE falls back to the `FullRescan` strategy with a `warning!()` logged. ### COR-3: WAL Decoder Slot Eligibility — Advisory Lock **Assessment finding:** C-3 (MEDIUM) **File:** `src/wal_decoder.rs` A new `pg_advisory_xact_lock` keyed on `BIGINT(slot_oid)` is acquired before calling `is_slot_suitable_for_wal_transition()` and released after `poll_wal_changes()` returns. The lock acquisition is done via: ```sql SELECT pg_advisory_xact_lock($1::bigint) ``` Any concurrent session attempting to drop the replication slot will block on the same lock, serialising the eligibility check and consumption into an atomic unit. ### COR-4: Compact-Buffer Contention Is Observable **Assessment finding:** C-4 (MEDIUM) **File:** `src/cdc.rs` — `compact_change_buffer_inner` **Before:** returned `Ok(0)` on `pg_try_advisory_xact_lock` failure, indistinguishable from "no rows to compact". **After:** returns `Ok(CompactionResult::Contended)`. The caller in the scheduler logs at `debug!` level and increments `pg_trickle_cdc_compact_contended_total` (a new `PgAtomic` in `src/shmem.rs`). The Prometheus endpoint exposes this counter so operators can detect persistent lock contention via alerting. --- ## Upgrade Notes No SQL schema changes. No `ALTER EXTENSION` migration is required. After upgrading, owners of stream tables who had previously used `attach_outbox` or `stream_table_to_publication` should verify that the operations complete as expected under the new ownership checks. Non-owner callers will now receive `ERROR: not stream table owner` instead of silently succeeding.