This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new cc83f7eb25 fix(arrow-cast): make `b64_encode` reject invalid UTF-8 
from misbehaving `Engine` impls (#10324)
cc83f7eb25 is described below

commit cc83f7eb25fbf0f6e8f94656d8fd703470e32c04
Author: Aditya Mishra <[email protected]>
AuthorDate: Sun Jul 19 07:51:13 2026 +0530

    fix(arrow-cast): make `b64_encode` reject invalid UTF-8 from misbehaving 
`Engine` impls (#10324)
    
    ## Which issue does this PR close?
    
    Fixes #10284
    
    ## Rationale for this change
    
    `b64_encode` builds a `GenericStringArray` from the output of a
    caller-supplied `base64::Engine` via `new_unchecked` guarded by a `//
    Safety: Base64 is valid UTF-8` comment. That comment only holds for a
    *correct* engine. `base64::Engine` is a safe trait so a caller can
    implement it in 100% safe Rust to write non-UTF-8 bytes into the buffer.
    The resulting `StringArray` then hands out an invalid `&str` which is UB
    reached from entirely safe code
    
    A safe fn has to be sound for all safe inputs so this is a soundness
    hole. It is not a remotely exploitable vuln: the trigger is a
    caller-supplied `Engine`, not untrusted data
    
    ## What changes are included in this PR?
    
    - `b64_encode` now returns `Result<GenericStringArray<O>, ArrowError>`
    and builds the array through the checked `GenericStringArray::try_new`
    constructor instead of `unsafe { new_unchecked(...) }`. A misbehaving
    engine now surfaces as an `Err` rather than UB. This matches
    `b64_decode`, which already returns `Result`
    - The `unsafe` block is gone
    - Updated the one in-tree caller (the `arrow-json` doc example)
    - Added a regression test with a safe-Rust `EvilEngine` (the issue's
    repro) that asserts `b64_encode` returns `Err`
    
    `b64_decode` is untouched. It returns `GenericBinaryArray`, which has no
    UTF-8 invariant, so this bug does not apply to it
    
    ## Verification
    
    - `cargo test -p arrow-cast -p arrow-json` passes.
    - Checked under Miri both ways: the `EvilEngine` repro aborts with UB
    (`char::from_u32_unchecked`) against the current `new_unchecked` code,
    and returns `Err` with no UB after this fix.
    
    ## Are there any user-facing changes?
    
    Yes, this is a breaking API change. `b64_encode` now returns `Result`,
    so callers have to handle the error (with `?` or `.unwrap()`).
    
    There is an alternative: mark `b64_encode` as `unsafe fn` and document
    the engine precondition. That has zero runtime cost but pushes `unsafe`
    onto every honest caller. I went with the `Result` route to keep the
    safe API safe, but I'm happy to switch to the `unsafe fn` shape if
    maintainers prefer it.
    
    Credit to the reporter (@Manishearth) for the Miri repro
---
 arrow-cast/src/base64.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 54 insertions(+), 5 deletions(-)

diff --git a/arrow-cast/src/base64.rs b/arrow-cast/src/base64.rs
index 6a8da0141d..ea53aa5dcc 100644
--- a/arrow-cast/src/base64.rs
+++ b/arrow-cast/src/base64.rs
@@ -27,7 +27,11 @@ use base64::engine::Config;
 
 pub use base64::prelude::*;
 
-/// Bas64 encode each element of `array` with the provided [`Engine`]
+/// Base64 encode each element of `array` with the provided [`Engine`]
+///
+/// Panics if the `Engine` emits output that is not valid UTF-8. A correct
+/// `Engine` never does, but it is a safe trait so a misbehaving impl could;
+/// validating keeps the returned [`GenericStringArray`] sound (#10284).
 pub fn b64_encode<E: Engine, O: OffsetSizeTrait>(
     engine: &E,
     array: &GenericBinaryArray<O>,
@@ -49,10 +53,9 @@ pub fn b64_encode<E: Engine, O: OffsetSizeTrait>(
     }
     assert_eq!(offset, buffer_len);
 
-    // Safety: Base64 is valid UTF-8
-    unsafe {
-        GenericStringArray::new_unchecked(offsets, Buffer::from_vec(buffer), 
array.nulls().cloned())
-    }
+    // `try_new` validates UTF-8 instead of trusting the (safe-trait) Engine.
+    GenericStringArray::try_new(offsets, Buffer::from_vec(buffer), 
array.nulls().cloned())
+        .expect("Engine produced invalid UTF-8")
 }
 
 /// Base64 decode each element of `array` with the provided [`Engine`]
@@ -113,4 +116,50 @@ mod tests {
         test_engine(&BASE64_STANDARD, &data);
         test_engine(&BASE64_STANDARD_NO_PAD, &data);
     }
+
+    /// Safe-Rust `Engine` that writes invalid UTF-8 into the encode buffer
+    /// (#10284). `b64_encode` must reject it rather than build an unsound
+    /// `StringArray`.
+    struct EvilEngine;
+
+    impl Engine for EvilEngine {
+        type Config = <base64::engine::GeneralPurpose as Engine>::Config;
+        type DecodeEstimate = <base64::engine::GeneralPurpose as 
Engine>::DecodeEstimate;
+
+        fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
+            BASE64_STANDARD.internal_encode(input, output)
+        }
+        fn internal_decoded_len_estimate(&self, input_len: usize) -> 
Self::DecodeEstimate {
+            BASE64_STANDARD.internal_decoded_len_estimate(input_len)
+        }
+        fn internal_decode(
+            &self,
+            input: &[u8],
+            output: &mut [u8],
+            estimate: Self::DecodeEstimate,
+        ) -> Result<base64::engine::DecodeMetadata, base64::DecodeSliceError> {
+            BASE64_STANDARD.internal_decode(input, output, estimate)
+        }
+        fn config(&self) -> &Self::Config {
+            BASE64_STANDARD.config()
+        }
+        fn encode_slice<T: AsRef<[u8]>>(
+            &self,
+            input: T,
+            output_buf: &mut [u8],
+        ) -> Result<usize, base64::EncodeSliceError> {
+            let len = BASE64_STANDARD.encode_slice(input, output_buf)?;
+            for b in output_buf[..len].iter_mut() {
+                *b = 0xFF; // invalid UTF-8, but correct length
+            }
+            Ok(len)
+        }
+    }
+
+    #[test]
+    #[should_panic(expected = "produced invalid UTF-8")]
+    fn test_b64_encode_rejects_invalid_utf8() {
+        let data: BinaryArray = 
vec![Some(b"hello".to_vec())].into_iter().collect();
+        let _ = b64_encode(&EvilEngine, &data);
+    }
 }

Reply via email to