timsaucer opened a new issue, #24722:
URL: https://github.com/apache/datafusion/issues/24722

   ## Describe the bug
   
   Several `datafusion-ffi` constructors unwrap an already-foreign input and 
return its original handle. Three of them drop the arguments passed alongside, 
without an error or a warning:
   
   | Constructor | Unwraps | Silently discards |
   |---|---|---|
   | `FFI_LogicalExtensionCodec::new` (`proto/logical_extension_codec.rs:297`) 
| `ForeignLogicalExtensionCodec` | `runtime`, `task_ctx_provider` |
   | `FFI_PhysicalExtensionCodec::new` 
(`proto/physical_extension_codec.rs:283`) | `ForeignPhysicalExtensionCodec` | 
`runtime`, `task_ctx_provider` |
   | `FFI_TableProvider::new_with_ffi_codec` (`table_provider.rs:405`) | 
`ForeignTableProvider` | `runtime`, `logical_codec` |
   
   ```rust
   pub fn new(
       codec: Arc<dyn LogicalExtensionCodec>,
       runtime: Option<Handle>,
       task_ctx_provider: impl Into<FFI_TaskContextProvider>,
   ) -> Self {
       if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
           .downcast_ref::<ForeignLogicalExtensionCodec>()
       {
           return codec.0.clone();   // runtime and task_ctx_provider are never 
read
       }
       ...
   ```
   
   Two sibling constructors hit the same case and do the opposite — they adopt 
the supplied values:
   
   `FFI_QueryPlanner::new_with_ffi_codecs` (`query_planner.rs:252`), whose doc 
comment makes the guarantee explicit:
   
   > If `planner` is already foreign, this re-exports its original FFI handle 
rather than adding another wrapper layer. The handle still adopts the codecs 
supplied here, so they are never silently discarded.
   
   ```rust
   if let Some(planner) = any_ref.downcast_ref::<ForeignQueryPlanner>() {
       let mut planner = planner.0.clone();
       planner.logical_codec = logical_codec;
       planner.physical_codec = physical_codec;
       return planner;
   }
   ```
   
   `FFI_SessionRef::new_with_ffi_codecs` (`session/mod.rs:481`) does the same 
for `logical_codec` and `physical_codec`.
   
   Given those two, the other three look like oversights rather than intent.
   
   ## To Reproduce
   
   A consumer that imports a foreign codec can never rebind it afterwards. 
Re-wrapping with a different `task_ctx_provider` compiles, runs, and has no 
effect:
   
   ```rust
   // P1 and P2 are distinct sessions.
   let ffi = FFI_LogicalExtensionCodec::new(my_codec, None, &p1_provider);
   let imported: Arc<dyn LogicalExtensionCodec> = (&ffi).into();   // 
ForeignLogicalExtensionCodec
   
   // Intent: rebind the imported codec to P2. Actual: no-op, still resolves P1.
   let rebound = FFI_LogicalExtensionCodec::new(imported, None, &p2_provider);
   ```
   
   (Sketch — I read this from the source rather than running it. Happy to add a 
failing unit test to the PR if that is useful.)
   
   The observable consequence, from `datafusion-python`:
   
   1. A user installs a foreign `LogicalExtensionCodec`. Its FFI handle holds a 
`Weak` to session **A**.
   2. The user then installs a foreign `QueryPlanner`. `datafusion-python` 
forks the session to **B** so the receiver is not mutated, and rebuilds its own 
outer codec wrapper against **B**.
   3. The inner foreign codec cannot be rebound, so it still points at **A**.
   4. Decode callbacks in the extension library resolve names against **A**'s 
registry. A UDF registered after the fork is invisible to them, and the config 
they see is the pre-fork snapshot.
   
   Worth noting where the correct context is lost: the host *does* pass the 
right `TaskContext` down —
   
   ```rust
   fn try_decode_table_provider(&self, buf, table_ref, schema, ctx: 
&TaskContext) -> Result<...> {
       self.inner.try_decode_table_provider(buf, table_ref, schema, ctx)   // 
ctx belongs to B
   }
   ```
   
   — but `try_decode_table_provider_fn_wrapper` takes no context parameter and 
calls `codec.task_ctx()` instead, so the argument is dropped at the boundary 
and the stale stored provider is substituted.
   
   There is a second failure mode with the same root cause. Because the 
discarded provider is held as a `Weak`, a consumer that cannot rebind must 
instead keep the original session alive artificially, or the capsule starts 
failing with `TaskContextProvider went out of scope over FFI boundary`.
   
   ## Expected behavior
   
   The three constructors adopt the supplied values on the unwrap path, 
matching `FFI_QueryPlanner::new_with_ffi_codecs`:
   
   ```rust
   if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
       .downcast_ref::<ForeignLogicalExtensionCodec>()
   {
       let mut codec = codec.0.clone();
       codec.task_ctx_provider = task_ctx_provider.into();
       return codec;
   }
   ```
   
   If discarding is deliberate for any of them, then the arguments should not 
be accepted silently — either document the behaviour on the constructor, or 
change the signature so a caller cannot pass a value that will be ignored.
   
   ## Additional context
   
   Found while adding FFI query planner support to `datafusion-python`: 
https://github.com/apache/datafusion-python/pull/1677
   
   The workaround there is to retain the pre-fork `SessionContext` so the 
`Weak` stays valid. That prevents the crash but not the staleness, and it 
retains memory that could otherwise be released. With the change above, the 
fork can rebind the inner codecs and the workaround is deleted.
   
   Code references are against the 55.x line (`branch-55`); line numbers should 
be confirmed against `main`.
   
   Separately, the `runtime` argument is discarded on the same paths. That is 
the same bug shape, and the same fix applies, though it has been less visible 
in practice.
   
   Related design question, if it is worth a separate issue: `try_decode` and 
`try_decode_table_provider` pull a `TaskContext` from the codec's stored 
provider even though the calling side usually has one in hand and already 
passes it to the trait method. Threading it through the FFI signature would 
make the stored provider unnecessary for those paths and remove the rebinding 
problem entirely, rather than making rebinding possible.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to