]> git.ipfire.org Git - thirdparty/tor.git/commitdiff
hashx: Rust hook for inspecting and modifying the random number stream
authorMicah Elizabeth Scott <beth@torproject.org>
Sat, 29 Jul 2023 02:44:24 +0000 (19:44 -0700)
committerMicah Elizabeth Scott <beth@torproject.org>
Sat, 29 Jul 2023 04:27:10 +0000 (21:27 -0700)
This patch has no effect on the C tor build.

Adds a function hashx_rng_callback() to the hashx API, defined only
when HASHX_RNG_CALLBACK is defined. This is then used in the Rust
wrapper to implement a similar rng_callback().

Included some minimal test cases. This code is intented for
use in cross-compatibility fuzzing tests which drive multiple
implementations of hashx with the same custom Rng stream.

Signed-off-by: Micah Elizabeth Scott <beth@torproject.org>
src/ext/equix/Cargo.toml
src/ext/equix/build.rs
src/ext/equix/hashx/include/hashx.h
src/ext/equix/hashx/src/context.c
src/ext/equix/hashx/src/program.c
src/ext/equix/hashx/src/program.h
src/ext/equix/hashx/src/siphash_rng.c
src/ext/equix/hashx/src/siphash_rng.h
src/ext/equix/src/lib.rs

index 45b24dcea166557fe7be09a34d1386d2bf27d349..bedb1ed5650480dbe7423e95e4edcc85fd2ab93d 100644 (file)
@@ -9,7 +9,7 @@
 
 [package]
 name = "tor-c-equix"
-version = "0.1.0"
+version = "0.2.0"
 edition = "2021"
 license = "LGPL-3.0-only"
 
index f2825a50cfc49510fd47b376da4afd565eb9b7e0..b53f08e8994b7b5e9ed5706b0c4da94d30aca230 100644 (file)
@@ -16,6 +16,8 @@ fn main() {
             "hashx/src/siphash_rng.c",
             "hashx/src/virtual_memory.c",
         ])
+        // Activate our patch for hashx_rng_callback
+        .define("HASHX_RNG_CALLBACK", "1")
         // Equi-X always uses HashX size 8 (64-bit output)
         .define("HASHX_SIZE", "8")
         // Avoid shared library API declarations, link statically
@@ -31,6 +33,7 @@ fn main() {
         .header_contents(
             "wrapper.h",
             r#"
+                #define HASHX_RNG_CALLBACK 1
                 #define HASHX_SIZE 8
                 #define HASHX_SHARED 1
                 #define EQUIX_SHARED 1
index 2910515d9a831605db97cfee82d94f22f49562fb..3f6d059b9276146f6b08465ee1c07cc92bdf1d68 100644 (file)
@@ -169,6 +169,25 @@ HASHX_API hashx_result hashx_exec(const hashx_ctx* ctx,
 */
 HASHX_API void hashx_free(hashx_ctx* ctx);
 
+#ifdef HASHX_RNG_CALLBACK
+/*
+ * Set a callback for inspecting or modifying the HashX random number stream.
+ *
+ * The callback and its user pointer are associated with the provided context
+ * even if it's re-used for another hash program. A callback value of NULL
+ * disables the callback.
+ *
+ * @param ctx is pointer to a HashX instance.
+ * @param callback is invoked after each new 64-bit pseudorandom value
+ *        is generated in a buffer. The callback may record it and/or replace
+ *        it. A NULL pointer here disables the callback.
+ * @param user_data is an opaque parameter given to the callback
+ */
+HASHX_API void hashx_rng_callback(hashx_ctx* ctx,
+                                  void (*callback)(uint64_t*, void*),
+                                  void* user_data);
+#endif
+
 #ifdef __cplusplus
 }
 #endif
index 03a9de57fdade39c70806fb66e065523604269ab..da1b997e16442412c7a1c352f6aa633995c270d2 100644 (file)
@@ -55,3 +55,13 @@ void hashx_free(hashx_ctx* ctx) {
                free(ctx);
        }
 }
+
+#ifdef HASHX_RNG_CALLBACK
+void hashx_rng_callback(hashx_ctx* ctx,
+                        void (*callback)(uint64_t*, void*),
+                        void* callback_user_data)
+{
+       ctx->program.rng_callback = callback;
+       ctx->program.rng_callback_user_data = callback_user_data;
+}
+#endif
index b44bdb855aab6fa458f7e01c436c9f1e02a760c1..1017d4070ab7a6050b4b70b7f6baeed0bb4ed9b1 100644 (file)
@@ -554,6 +554,10 @@ bool hashx_program_generate(const siphash_state* key, hashx_program* program) {
                .ports = {{ 0 }}
        };
        hashx_siphash_rng_init(&ctx.gen, key);
+#ifdef HASHX_RNG_CALLBACK
+       ctx.gen.callback = program->rng_callback;
+       ctx.gen.callback_user_data = program->rng_callback_user_data;
+#endif
        for (int i = 0; i < 8; ++i) {
                ctx.registers[i].last_op = -1;
                ctx.registers[i].latency = 0;
index 096cc4ee0a03268cd098346f07a66c3e274a518a..78dbb8b6e37e18a7ee6c89846abc58e168966b0e 100644 (file)
@@ -29,6 +29,10 @@ typedef struct hashx_program {
        int branch_count;
        int branches[16];
 #endif
+#ifdef HASHX_RNG_CALLBACK
+       void (*rng_callback)(uint64_t *buffer, void *user_data);
+       void *rng_callback_user_data;
+#endif
 } hashx_program;
 
 #ifdef __cplusplus
index 89ed8fc8450fd7f976c51cbfb41d062b0d5fbcbe..c0f457be76e8221b04215c97788139dea169e2d1 100644 (file)
@@ -15,6 +15,11 @@ uint8_t hashx_siphash_rng_u8(siphash_rng* gen) {
                gen->buffer8 = hashx_siphash13_ctr(gen->counter, &gen->keys);
                gen->counter++;
                gen->count8 = sizeof(gen->buffer8);
+#ifdef HASHX_RNG_CALLBACK
+               if (gen->callback) {
+                       gen->callback(&gen->buffer8, gen->callback_user_data);
+               }
+#endif
        }
        gen->count8--;
        return gen->buffer8 >> (gen->count8 * 8);
@@ -25,6 +30,11 @@ uint32_t hashx_siphash_rng_u32(siphash_rng* gen) {
                gen->buffer32 = hashx_siphash13_ctr(gen->counter, &gen->keys);
                gen->counter++;
                gen->count32 = sizeof(gen->buffer32) / sizeof(uint32_t);
+#ifdef HASHX_RNG_CALLBACK
+               if (gen->callback) {
+                       gen->callback(&gen->buffer32, gen->callback_user_data);
+               }
+#endif
        }
        gen->count32--;
        return (uint32_t)(gen->buffer32 >> (gen->count32 * 32));
index 638b177e06795f4967dbd493b94637172d29abeb..7b402fdc6e87cd8a3c7c1e7fbac3674ce034b5ab 100644 (file)
@@ -13,6 +13,10 @@ typedef struct siphash_rng {
        uint64_t counter;
        uint64_t buffer8, buffer32;
        unsigned count8, count32;
+#ifdef HASHX_RNG_CALLBACK
+       void (*callback)(uint64_t *buffer, void *user_data);
+       void *callback_user_data;
+#endif
 } siphash_rng;
 
 #ifdef __cplusplus
index 0db1fc1bb32d437241aa9daa161f5a595783fe2f..8eb163075a5bd2271704449d076fba5c3cd894ba 100644 (file)
 //! See `LICENSE` for licensing information.
 //!
 
+use core::ffi::c_void;
+use core::mem;
+use core::ptr::null_mut;
+
 pub mod ffi {
     //! Low-level access to the C API
 
@@ -34,8 +38,14 @@ pub const HASHX_SIZE: usize = ffi::HASHX_SIZE as usize;
 /// Output value obtained by executing a HashX hash function
 pub type HashXOutput = [u8; HASHX_SIZE];
 
+/// Type for callback functions that inspect or replace the pseudorandom stream
+pub type RngCallback = Box<dyn FnMut(u64) -> u64>;
+
 /// Safe wrapper around a HashX context
-pub struct HashX(*mut ffi::hashx_ctx);
+pub struct HashX {
+    ctx: *mut ffi::hashx_ctx,
+    rng_callback: Option<RngCallback>,
+}
 
 impl HashX {
     /// Allocate a new HashX context
@@ -44,7 +54,10 @@ impl HashX {
         if ctx.is_null() {
             panic!("out of memory in hashx_alloc");
         }
-        Self(ctx)
+        Self {
+            ctx,
+            rng_callback: None,
+        }
     }
 
     /// Create a new hash function within this context, using the given seed
@@ -53,14 +66,15 @@ impl HashX {
     /// error occurs while the interpreter is disabled.
     #[inline(always)]
     pub fn make(&mut self, seed: &[u8]) -> HashXResult {
-        unsafe { ffi::hashx_make(self.0, seed.as_ptr() as *const std::ffi::c_void, seed.len()) }
+        unsafe { ffi::hashx_make(self.ctx, seed.as_ptr() as *const c_void, seed.len()) }
     }
 
     /// Check which implementation was selected by `make`
     #[inline(always)]
     pub fn query_type(&mut self) -> Result<HashXType, HashXResult> {
         let mut buffer = HashXType::HASHX_TYPE_INTERPRETED; // Arbitrary default
-        let result = unsafe { ffi::hashx_query_type(self.0, &mut buffer as *mut ffi::hashx_type) };
+        let result =
+            unsafe { ffi::hashx_query_type(self.ctx, &mut buffer as *mut ffi::hashx_type) };
         match result {
             HashXResult::HASHX_OK => Ok(buffer),
             e => Err(e),
@@ -71,23 +85,45 @@ impl HashX {
     #[inline(always)]
     pub fn exec(&mut self, input: u64) -> Result<HashXOutput, HashXResult> {
         let mut buffer: HashXOutput = Default::default();
-        let result = unsafe {
-            ffi::hashx_exec(
-                self.0,
-                input,
-                &mut buffer as *mut u8 as *mut std::ffi::c_void,
-            )
-        };
+        let result =
+            unsafe { ffi::hashx_exec(self.ctx, input, &mut buffer as *mut u8 as *mut c_void) };
         match result {
             HashXResult::HASHX_OK => Ok(buffer),
             e => Err(e),
         }
     }
+
+    /// Set a callback function that may inspect and/or modify the internal
+    /// pseudorandom number stream used by this context.
+    ///
+    /// The function will be owned by this context, and it replaces any
+    /// previous function that may have been set. Returns the previous callback
+    /// if any.
+    pub fn rng_callback(&mut self, callback: Option<RngCallback>) -> Option<RngCallback> {
+        // Keep ownership of our Rust value in the context wrapper, to match
+        // the lifetime of the mutable pointer that the C API saves.
+        let result = mem::replace(&mut self.rng_callback, callback);
+        match &mut self.rng_callback {
+            None => unsafe { ffi::hashx_rng_callback(self.ctx, None, null_mut()) },
+            Some(callback) => unsafe {
+                ffi::hashx_rng_callback(
+                    self.ctx,
+                    Some(wrapper),
+                    callback as *mut RngCallback as *mut c_void,
+                );
+            },
+        }
+        unsafe extern "C" fn wrapper(buffer: *mut u64, callback: *mut c_void) {
+            let callback: &mut RngCallback = unsafe { mem::transmute(callback) };
+            buffer.write(callback(buffer.read()));
+        }
+        result
+    }
 }
 
 impl Drop for HashX {
     fn drop(&mut self) {
-        let ctx = std::mem::replace(&mut self.0, std::ptr::null_mut());
+        let ctx = mem::replace(&mut self.ctx, null_mut());
         unsafe {
             ffi::hashx_free(ctx);
         }
@@ -146,7 +182,7 @@ impl EquiX {
         unsafe {
             ffi::equix_verify(
                 self.0,
-                challenge.as_ptr() as *const std::ffi::c_void,
+                challenge.as_ptr() as *const c_void,
                 challenge.len(),
                 solution as *const ffi::equix_solution,
             )
@@ -159,7 +195,7 @@ impl EquiX {
         unsafe {
             ffi::equix_solve(
                 self.0,
-                challenge.as_ptr() as *const std::ffi::c_void,
+                challenge.as_ptr() as *const c_void,
                 challenge.len(),
                 buffer as *mut ffi::equix_solutions_buffer,
             )
@@ -169,7 +205,7 @@ impl EquiX {
 
 impl Drop for EquiX {
     fn drop(&mut self) {
-        let ctx = std::mem::replace(&mut self.0, std::ptr::null_mut());
+        let ctx = mem::replace(&mut self.0, null_mut());
         unsafe {
             ffi::equix_free(ctx);
         }
@@ -180,6 +216,8 @@ impl Drop for EquiX {
 mod tests {
     use crate::*;
     use hex_literal::hex;
+    use std::cell::RefCell;
+    use std::sync::Arc;
 
     #[test]
     fn equix_context() {
@@ -290,4 +328,52 @@ mod tests {
         assert_eq!(ctx.exec(123456), Ok(hex!("ab3d155bf4bbb0aa")));
         assert_eq!(ctx.exec(987654321123456789), Ok(hex!("8dfef0497c323274")));
     }
+
+    #[test]
+    fn rng_callback_read() {
+        // Use a Rng callback to read the sequence of pseudorandom numbers
+        // without changing them, and spot check the list we get back.
+        let mut ctx = HashX::new(HashXType::HASHX_TRY_COMPILE);
+        let seq = Arc::new(RefCell::new(Vec::new()));
+        {
+            let seq = seq.clone();
+            ctx.rng_callback(Some(Box::new(move |value| {
+                seq.borrow_mut().push(value);
+                value
+            })));
+        }
+        assert_eq!(seq.borrow().len(), 0);
+        assert_eq!(ctx.make(b"abc"), HashXResult::HASHX_OK);
+        assert_eq!(ctx.exec(12345).unwrap(), hex!("c0bc95da7cc30f37"));
+        assert_eq!(seq.borrow().len(), 563);
+        assert_eq!(
+            seq.borrow()[..4],
+            [
+                0xf695edd02205449d,
+                0x51c1ac51cd19a7d1,
+                0xadf4cb303b9814cf,
+                0x79793a52d965083d
+            ]
+        );
+    }
+
+    #[test]
+    fn rng_callback_replace() {
+        // Use a Rng callback to replace the random number stream.
+        // We have to choose the replacement somewhat carefully since
+        // many stationary replacement values will cause infinite loops.
+        let mut ctx = HashX::new(HashXType::HASHX_TYPE_INTERPRETED);
+        let counter = Arc::new(RefCell::new(0u32));
+        {
+            let counter = counter.clone();
+            ctx.rng_callback(Some(Box::new(move |_value| {
+                *counter.borrow_mut() += 1;
+                0x0807060504030201
+            })));
+        }
+        assert_eq!(*counter.borrow(), 0);
+        assert_eq!(ctx.make(b"abc"), HashXResult::HASHX_OK);
+        assert_eq!(ctx.exec(12345).unwrap(), hex!("825a9b6dd5d074af"));
+        assert_eq!(*counter.borrow(), 575);
+    }
 }