---
phase: 65.1-phase-65-code-review-remediation
plan: 02b
type: execute
wave: 2
depends_on:
  - 65.1-02a
  - 65.1-01
files_modified:
  - src/lib.rs
  - tests/registration_error_surfaces.rs
autonomous: true
requirements:
  - REM-WR-02

must_haves:
  truths:
    - "`src/lib.rs::init_extension` allocates a 1024-byte error_buf per registration call, propagates buffer contents into the returned `Err(...)` on failure"
    - "Every `sv_register_*` extern declaration in `src/lib.rs` whose underlying C function was updated in Plan 02a carries `error_buf: *mut c_char, error_buf_len: usize` trailing parameters"
    - "Integration test `tests/registration_error_surfaces.rs::init_extension_surfaces_registration_error_buf` calls `sv_register_table_function` with a null `init_cb` and asserts the returned error string contains `null required argument`"
    - "Pre-existing typed no-op stub functions (NOT `std::mem::transmute`) supply the dummy bind_cb/exec_cb signatures (W-01 fix: avoid transmute footgun even though the D-05 null-init refusal fires first)"
  artifacts:
    - path: src/lib.rs
      provides: "init_extension call sites passing error_buf + propagating message on failure; updated extern declarations"
      contains: "error_buf"
    - path: tests/registration_error_surfaces.rs
      provides: "Force-fail registration test exercising the error_buf surfacing; uses typed no-op stubs (no transmute)"
      contains: "null required argument"
  key_links:
    - from: src/lib.rs
      to: cpp/src/shim.cpp::sv_register_table_function
      via: "FFI extern declaration in shim.hpp (updated by Plan 02a)"
      pattern: "sv_register_table_function"
    - from: tests/registration_error_surfaces.rs
      to: cpp/src/shim.cpp::sv_register_table_function D-05 null-init refusal
      via: "force-fail via deliberately-null init_cb"
      pattern: "init_cb is mandatory"
---

<objective>
Cascade Plan 02a's C ABI changes into the Rust side. Update every `sv_register_*` extern declaration and call site in `src/lib.rs::init_extension` to allocate a 1024-byte error_buf, pass the trailing pointer pair, and parse failures via `CStr::from_ptr(...).to_string_lossy()`. Populate the Wave 0 stub `tests/registration_error_surfaces.rs` (from Plan 01) with the actual force-fail assertion.

This is the **Rust-side half** of the WR-02 work. Plan 02a updated the C ABI; this plan consumes the new ABI from `init_extension` (~17 call sites — mechanical cascade) and adds the integration test that proves the error_buf path surfaces a real failure (null init_cb refused per D-05).

W-01 fix (plan-checker feedback): use **pre-defined typed no-op stub functions** matching the real `bind_cb`/`exec_cb` signatures rather than `std::mem::transmute`. The D-05 null-init_cb check fires before either stub is invoked, so the dummies are never called — but typed signatures eliminate the transmute footgun for future readers.

Wave 2 because it depends on:
- Plan 02a (the C ABI surface this Rust side targets — wave 1)
- Plan 01 (the `tests/registration_error_surfaces.rs` stub this plan populates — wave 1)

Implements D-08, D-09 (WR-02 Rust-side).
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-CONTEXT.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-RESEARCH.md
@.planning/phases/65.1-phase-65-code-review-remediation/65.1-PATTERNS.md
@cpp/src/shim.hpp
@src/lib.rs
@tests/no_long_lived_conn.rs
@CLAUDE.md
</context>

<interfaces>
<!-- After Plan 02a lands, the C ABI is: -->

Updated `sv_register_table_function` (post Plan 02a):
```
bool sv_register_table_function(
    duckdb_database db_handle,
    const char *name,
    const LogicalType *arg_types,
    size_t arg_count,
    table_function_bind_t bind_cb,
    table_function_t exec_cb,
    table_function_init_local_t init_cb,
    char *error_buf, size_t error_buf_len);
```

Rust caller-side allocation pattern (per `src/lib.rs` existing `sv_register_parser_hooks` call shape):
```
let mut error_buf = [0u8; 1024];
let ok = unsafe { sv_register_table_function(
    db_handle, ..., error_buf.as_mut_ptr() as *mut c_char, error_buf.len()) };
if !ok {
    let msg = unsafe { CStr::from_ptr(error_buf.as_ptr() as *const c_char) }
        .to_string_lossy().into_owned();
    return Err(format!("Failed to register <name>: {msg}").into());
}
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Update src/lib.rs::init_extension call sites + extern signatures</name>
  <read_first>
    - src/lib.rs lines 395-575 (the registration sequence — research enumerates ~17 wrapper call sites)
    - cpp/src/shim.hpp (updated declarations from Plan 02a)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-RESEARCH.md lines 365-375 (caller-side Rust shape template)
    - src/ddl/read_ffi.rs lines 84-95 (existing `write_err` helper showing the snprintf-equivalent Rust convention)
  </read_first>
  <files>src/lib.rs</files>
  <action>
1. Locate the `extern "C"` block in `src/lib.rs` (or wherever the `sv_register_*` extern declarations live in this crate — research notes they live near `init_extension`). Append `error_buf: *mut c_char, error_buf_len: usize` to every `sv_register_*` extern declaration whose underlying C function was updated in Plan 02a.

2. For every call site in `init_extension`:
   - Immediately BEFORE the call, allocate `let mut error_buf = [0u8; 1024];`.
   - Append the buffer pointer pair as trailing args: `..., error_buf.as_mut_ptr().cast::<c_char>(), error_buf.len()`.
   - Replace the existing `if !ok { return Err("Failed to register <name>".into()); }` (or equivalent) with the shape from `<interfaces>` above — parse the buffer via `CStr::from_ptr(error_buf.as_ptr() as *const c_char).to_string_lossy().into_owned()` and embed in the `Err(...)` message as `"Failed to register <name>: {msg}"`.
   - If the buffer is all-zeros (empty), fall back to `"Failed to register <name>: (no error text)"` so we never produce a misleading empty suffix.

3. Add `use std::ffi::CStr;` and `use std::os::raw::c_char;` imports if not already present.

4. The `sv_register_parser_hooks` call already follows this convention — DO NOT modify it here. The WR-09 fix (Plan 12) further modifies that block. The WR-06 runtime probe (Plan 10) also lives inside that helper but is C-side.

5. Keep ordering identical — this plan does NOT reorder, dedup, or wrap the registration sequence. Plan 12 owns WR-09 idempotence.

Verify the build compiles after each cluster.
  </action>
  <verify>
    <automated>cargo build --features extension &gt; /tmp/claude/p02b_lib.log 2>&amp;1; RC=$?; tail -60 /tmp/claude/p02b_lib.log; test $RC -eq 0</automated>
  </verify>
  <acceptance_criteria>
    - Every `sv_register_*` extern declaration in `src/lib.rs` (matching one updated in Plan 02a) ends with `error_buf: *mut c_char, error_buf_len: usize`
    - Every call site in `init_extension` allocates a `[0u8; 1024]` buffer, passes the pointer pair, and parses failures via `CStr::from_ptr(...).to_string_lossy()`
    - `grep -c "let mut error_buf" src/lib.rs` returns at least the count of `sv_register_*` call sites updated (typically ~17)
    - No `Err("Failed to register <name>".into())` literals remain — every failure path includes the parsed buffer text
    - `cargo build --features extension` exits 0
  </acceptance_criteria>
  <done>All init_extension registrations propagate the underlying error text in the returned `Err(...)`.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Populate tests/registration_error_surfaces.rs with force-fail assertion (typed stubs, no transmute)</name>
  <behavior>
    - Calling `sv_register_table_function` with all required args present BUT `init_cb = None` (null) returns `false` AND populates `error_buf` with text containing the substring `init_cb is mandatory`
    - Dummy `bind_cb` / `exec_cb` are pre-defined Rust functions with the correct extern "C" FFI signatures (NO `std::mem::transmute` — W-01 fix)
    - Even though the dummies are never invoked (D-05 null-init refusal fires first), their typed signatures eliminate the transmute footgun
    - Build/test runs under `cargo test --test registration_error_surfaces` and exits 0 with `1 passed`
  </behavior>
  <read_first>
    - tests/registration_error_surfaces.rs (the Wave 0 stub from Plan 01)
    - src/lib.rs (to see how the extern declaration for `sv_register_table_function` is exposed; mirror the same extern in the test, using the typed function pointer aliases from libduckdb_sys for bind_cb/exec_cb)
    - tests/no_long_lived_conn.rs (template for top-level Rust integration test — no_long_lived_conn shows `#[test]` shape and how to use `libduckdb_sys` from a `tests/*.rs` integration test)
    - .planning/phases/65.1-phase-65-code-review-remediation/65.1-CONTEXT.md (D-08, D-09)
    - cpp/src/shim.hpp (the post-Plan-02a declarations — confirm the exact function-pointer typedefs `table_function_bind_t`, `table_function_t`, `table_function_init_local_t` and their signatures so the Rust-side stubs match)
  </read_first>
  <files>tests/registration_error_surfaces.rs</files>
  <action>
Replace the stub body. The test creates a real in-memory DuckDB, obtains a `duckdb_database` handle, then calls `sv_register_table_function` with `init_cb = None` (null) and asserts:

1. The function returned `false`.
2. The error buffer contains the substring `init_cb is mandatory`.

**W-01 fix — no `std::mem::transmute`:** The original draft used `Some(std::mem::transmute(dummy as extern "C" fn()))` to coerce a no-arg extern fn into the bind_cb/exec_cb signatures. That compiles but is a segfault hazard if the D-05 null-init check is ever weakened: the casts would let the wrong-arity function be invoked. Instead, define typed no-op stubs that match the real callback signatures.

Implementation outline (the executor must adapt the exact signatures to match what Plan 02a's shim.hpp ended up declaring — the `table_function_bind_t`, `table_function_t` typedefs in shim.hpp are the source of truth):

```rust
//! Phase 65.1 Plan 02b — WR-02 D-08/D-09 + CR-02 D-05.
//! Verifies the error_buf ABI surfaces underlying registration failures.

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;

// Typed no-op stubs matching the real callback signatures.
// W-01 fix: these are NEVER invoked at runtime — the D-05 null-init check in
// sv_register_table_function fires first — but the typed signatures prevent
// the transmute footgun. If D-05 is ever relaxed, calling these is a clean
// no-op rather than UB from arity mismatch.

// Adjust the parameter list to match `table_function_bind_t` from shim.hpp
// exactly. Example shape (verify against shim.hpp before committing):
unsafe extern "C" fn noop_bind(
    _ctx: *mut std::ffi::c_void,
    _input: *mut std::ffi::c_void,
    _bind_data_out: *mut *mut std::ffi::c_void,
) {
    // intentionally empty — never invoked
}

unsafe extern "C" fn noop_exec(
    _ctx: *mut std::ffi::c_void,
    _input: *mut std::ffi::c_void,
    _output: *mut std::ffi::c_void,
) {
    // intentionally empty — never invoked
}

// Extern declaration mirrors src/lib.rs — the symbol is exported by the
// extension's static lib and resolvable at test link time when the test
// is built without the cdylib feature.
extern "C" {
    fn sv_register_table_function(
        db_handle: libduckdb_sys::duckdb_database,
        name: *const c_char,
        arg_types: *const std::ffi::c_void, // LogicalType opaque to Rust
        arg_count: usize,
        bind_cb: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *mut std::ffi::c_void, *mut *mut std::ffi::c_void)>,
        exec_cb: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *mut std::ffi::c_void, *mut std::ffi::c_void)>,
        init_cb: Option<unsafe extern "C" fn(*mut std::ffi::c_void, *mut std::ffi::c_void) -> *mut std::ffi::c_void>,
        error_buf: *mut c_char,
        error_buf_len: usize,
    ) -> bool;
}

#[test]
fn init_extension_surfaces_registration_error_buf() {
    let mut db: libduckdb_sys::duckdb_database = ptr::null_mut();
    let rc = unsafe { libduckdb_sys::duckdb_open(ptr::null(), &mut db) };
    assert_eq!(rc, libduckdb_sys::DuckDBSuccess, "duckdb_open failed");

    let name = CString::new("test_force_fail_tf").unwrap();

    let mut error_buf = [0u8; 1024];
    let ok = unsafe {
        sv_register_table_function(
            db,
            name.as_ptr(),
            ptr::null(),
            0,
            Some(noop_bind),
            Some(noop_exec),
            None, // ← deliberately null init_cb — must trigger D-05 refusal
            error_buf.as_mut_ptr().cast::<c_char>(),
            error_buf.len(),
        )
    };

    unsafe { libduckdb_sys::duckdb_close(&mut db) };

    assert!(!ok, "registration must refuse null init_cb");
    let msg = unsafe { CStr::from_ptr(error_buf.as_ptr().cast::<c_char>()) }
        .to_string_lossy()
        .into_owned();
    assert!(
        msg.contains("init_cb is mandatory"),
        "error_buf must contain `init_cb is mandatory` substring; got: {msg}"
    );
}
```

NOTE on signature matching: the exact `noop_bind` / `noop_exec` / `init_cb` parameter lists MUST match the C typedefs `table_function_bind_t` / `table_function_t` / `table_function_init_local_t` from `cpp/src/shim.hpp`. Read those typedefs BEFORE writing the Rust stubs — even a single arity mismatch defeats the W-01 fix. If the typedefs use richer DuckDB types (`TableFunctionBindInput*`, `DataChunk*`, etc.), use the opaque `*mut std::ffi::c_void` Rust equivalent at the FFI boundary; what matters is the count of parameters and overall layout, not the type identity.

If the build resolves the symbol differently (e.g. mangled by the `extension` feature), adapt by gating with `#[cfg(not(feature = "extension"))]` or by re-using a Rust-side helper exposed in `src/lib.rs` for testing — discretion to whichever resolves cleanly given the existing crate structure (no_long_lived_conn.rs uses static analysis only, so this is the first test that actually links to the FFI symbols; the executor may need to add a `#[cfg(test)]` re-export in `src/lib.rs` if direct linkage fails).

The test must FAIL on the pre-Plan-02a binary (no `init_cb is mandatory` text) — verify by running BEFORE the fix landed if possible, or document inability to do so.
  </action>
  <verify>
    <automated>cargo test --test registration_error_surfaces &gt; /tmp/claude/p02b_test.log 2>&amp;1; RC=$?; tail -30 /tmp/claude/p02b_test.log; test $RC -eq 0 &amp;&amp; grep -q "init_extension_surfaces_registration_error_buf ... ok" /tmp/claude/p02b_test.log &amp;&amp; ! grep -q "std::mem::transmute" tests/registration_error_surfaces.rs</automated>
  </verify>
  <acceptance_criteria>
    - `tests/registration_error_surfaces.rs` no longer contains the `STUB:` marker
    - One `#[test] fn init_extension_surfaces_registration_error_buf()` exists with an actual force-fail assertion
    - Test invokes `sv_register_table_function` with `init_cb = None`
    - Test asserts the returned bool is `false`
    - Test asserts `error_buf` contents (after CStr conversion) contain the substring `init_cb is mandatory`
    - `cargo test --test registration_error_surfaces` exits 0 with `1 passed`
    - `grep -q "std::mem::transmute" tests/registration_error_surfaces.rs` returns false (W-01 fix verified)
    - Dummy bind_cb/exec_cb are defined as typed extern "C" fn matching the C typedef parameter shape
  </acceptance_criteria>
  <done>WR-02 + CR-02 D-05 enforcement is exercised by a real integration test that fails loud if the error_buf or null-init refusal regress. Test uses typed no-op stubs (no transmute) per W-01 fix.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Rust ↔ C++ FFI | `sv_register_*` calls cross the FFI boundary; failures must surface via ABI-stable channel (error_buf) |
| Test-side function pointers | Dummy bind/exec callbacks must match the C typedef arity, even though never invoked |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-65.1-02b-01 | Tampering | Future weakening of D-05 lets noop_bind/noop_exec be invoked | mitigate | Typed extern "C" fn matching the real callback signatures means invocation is a clean no-op rather than UB from arity mismatch (W-01 fix) |
| T-65.1-02b-02 | Information disclosure | Error message via error_buf may include path or query fragments | accept | The error text is bounded to 1024 bytes and the buffer is caller-owned; no secret material expected in registration-failure context |
</threat_model>

<verification>
  <automated>just test-all &gt; /tmp/claude/p02b_all.log 2>&amp;1; RC=$?; tail -100 /tmp/claude/p02b_all.log; test $RC -eq 0</automated>
</verification>

<success_criteria>
- `init_extension` registrations propagate underlying error text in returned `Err(...)`
- `tests/registration_error_surfaces.rs::init_extension_surfaces_registration_error_buf` passes
- No `std::mem::transmute` in the test file (W-01 fix)
- `just test-all` green
</success_criteria>

<output>
Create `.planning/phases/65.1-phase-65-code-review-remediation/65.1-02b-SUMMARY.md` when done.
</output>
