├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── feedback_vs_none.png ├── output_feedback.txt ├── output_nofeedback.txt ├── src ├── main.rs └── rng.rs └── verify.plt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "fuzzyneural" 5 | version = "0.1.0" 6 | 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzzyneural" 3 | version = "0.1.0" 4 | authors = ["Brandon Falk "] 5 | edition = "2018" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gamozo Labs, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | fuzzyneural is a neural network implementation in Rust that does not use backpropagation. Well, that sounds terrible? Yeah, it is. But the goal of this project is to emulate code-coverage style feedback models in a neural network to demonstrate the impact of code coverage and feedback. 4 | 5 | I've wanted to demonstrate the machine-learning properties of code coverage + feedback in fuzzing in a bit more robust way, and this is the project where I will attempt to do that. 6 | 7 | This has been a project idea I've had for a while. Since fuzzing is typically very stepwise (only a few crashes per target to validate "success"), neural networks have a bonus of having a nearly infinite gradient from error functions. 8 | 9 | # Feedback 10 | 11 | What is feedback? Well in the case of code coverage, it's saving off inputs that caused new state in the program. These inputs are then saved and built upon. 12 | 13 | In this neural network implementation we use feedback + random mutations rather than standard back-propagation. This should be much worse than back-propagation, but it's still leagues better than no feedback. 14 | 15 | ## How is feedback implemented in this network 16 | 17 | - A random network is configured (random weights and biases) 18 | - 10: 19 | - Copy network from the best-known-network 20 | - 1 weight is randomly mutated per layer 21 | - The error rate is checked based on the new network 22 | - If the error rate is lower than the previous record, save the network as the new best 23 | - Goto 10 24 | 25 | This feedback model is extremely similar to the most basic fuzzing feedback mechanisms, and shows how impactful of an algorithm this is. Even though it's super simple. 26 | 27 | # Conclusion 28 | 29 | ## Basic feedback vs no feedback 30 | 31 | Here is a graph showing the frequencies of time-to-perfect-fit for the network for basic feedback vs no feedback. This shows how much faster networks are found with this basic feedback mechanism 32 | 33 | ![asdf](assets/feedback_vs_none.png) 34 | -------------------------------------------------------------------------------- /assets/feedback_vs_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamozolabs/fuzzyneural/a59a8fd810876fff3a65539776059100bcf74211/assets/feedback_vs_none.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod rng; 2 | 3 | use std::time::Instant; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use crate::rng::Rng; 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub enum Layer { 10 | Forward { 11 | /// Number of hidden values at this layer 12 | size: usize, 13 | 14 | /// Weights for this layer 15 | /// Number of weights should be `size` * `previous_layer_size` 16 | weights: Vec, 17 | }, 18 | 19 | Bias { 20 | /// Biases (value to add to all inputs from the previous layer) 21 | /// Number of biases should be equal to `size` which should be identical 22 | /// to the previous layer size. 23 | biases: Vec, 24 | }, 25 | 26 | LinStep, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct Network { 31 | /// All the layers in the neural network, in order 32 | layers: Vec, 33 | 34 | /// Number of input values 35 | size: usize, 36 | 37 | /// Neurons for even numbered layers 38 | /// Inputs start in this layer 39 | layer_even: Vec, 40 | 41 | /// Neurons for odd numbered layers 42 | layer_odd: Vec, 43 | 44 | /// Random number generator 45 | rng: Rng, 46 | 47 | /// Scratch storage for diffs applied by the most recent mutation 48 | mutate_diffs: Vec<(usize, usize, f32, f32)>, 49 | } 50 | 51 | impl Network { 52 | /// Create a new, empty, neural network. Input set size based on `size` 53 | pub fn new(size: usize) -> Self { 54 | Network { 55 | layers: Vec::new(), 56 | size: size, 57 | layer_even: vec![0.; size], // initially the input layer 58 | layer_odd: Vec::new(), 59 | rng: Rng::new(), 60 | mutate_diffs: Vec::new(), 61 | } 62 | } 63 | 64 | /// Adds a layer to the neural network 65 | pub fn add_layer(&mut self, mut layer: Layer) { 66 | // Validate the network weights and sizes line up 67 | let mut cur_size = self.size; 68 | for layer in self.layers.iter() { 69 | match layer { 70 | Layer::Forward { size, weights } => { 71 | assert!(weights.len() == (cur_size * size), 72 | "Invalid number of weights for layer"); 73 | cur_size = *size; 74 | } 75 | Layer::Bias { biases } => { 76 | assert!(biases.len() == cur_size, 77 | "Invalid number of biases"); 78 | } 79 | Layer::LinStep => {} 80 | } 81 | } 82 | 83 | // Initialize the weights 84 | match &mut layer { 85 | Layer::Forward { size, weights } => { 86 | // Initialize weights 87 | weights.clear(); 88 | for _ in 0..(*size * cur_size) { 89 | weights.push(self.rng.rand_f32(-1.0, 1.0)); 90 | } 91 | } 92 | Layer::Bias { biases } => { 93 | // Initialize biases 94 | biases.clear(); 95 | for _ in 0..cur_size { 96 | biases.push(self.rng.rand_f32(-1.0, 1.0)); 97 | } 98 | } 99 | Layer::LinStep => {} 100 | } 101 | 102 | // Add the layer! 103 | self.layers.push(layer); 104 | } 105 | 106 | /// Randomly mutate weights, returns a slice identifying the weights 107 | /// which were changed. 108 | /// Modification tuple is (layer_id, weight_id, prev_weight, new_weight) 109 | pub fn mutate_weights(&mut self) { 110 | // Clear the diff log 111 | self.mutate_diffs.clear(); 112 | 113 | for (layer_id, layer) in self.layers.iter_mut().enumerate() { 114 | match layer { 115 | Layer::Forward { weights, .. } => { 116 | let pick = self.rng.rand() as usize % weights.len(); 117 | let prev = weights[pick]; 118 | weights[pick] = self.rng.rand_f32(-1.0, 1.0); 119 | self.mutate_diffs 120 | .push((layer_id, pick, prev, weights[pick])); 121 | } 122 | Layer::Bias { biases } => { 123 | let pick = self.rng.rand() as usize % biases.len(); 124 | let prev = biases[pick]; 125 | biases[pick] = self.rng.rand_f32(-1.0, 1.0); 126 | self.mutate_diffs 127 | .push((layer_id, pick, prev, biases[pick])); 128 | } 129 | Layer::LinStep => {} 130 | } 131 | } 132 | } 133 | 134 | /// Propagates the input layer through the network, returning a reference 135 | /// to the resulting output layer 136 | pub fn forward_propagate(&mut self) -> &[f32] { 137 | let mut cur_size = self.size; 138 | 139 | for (layer_depth, layer) in self.layers.iter().enumerate() { 140 | // Alternate the double buffers depending on which layer we are 141 | // processing 142 | let (input_layer, output_layer) = if (layer_depth & 1) == 0 { 143 | (&self.layer_even, &mut self.layer_odd) 144 | } else { 145 | (&self.layer_odd, &mut self.layer_even) 146 | }; 147 | 148 | // Clear the output layer we're about to fill up 149 | output_layer.clear(); 150 | 151 | match layer { 152 | Layer::Forward { size, weights } => { 153 | // Initialize the current weight index to zero 154 | let mut weight_id = 0; 155 | 156 | // For each neuron at this layer 157 | for _output_id in 0..*size { 158 | // Create a new accumulator 159 | let mut neuron = 0f32; 160 | 161 | // Go through each input from the previous layer, and 162 | // multiply it by the corresponding weight 163 | for input_id in 0..cur_size { 164 | // Multiply against weight and add to existing 165 | // accumulator 166 | neuron += 167 | input_layer[input_id] * weights[weight_id]; 168 | 169 | // Update weight index to the next weight 170 | weight_id += 1; 171 | } 172 | 173 | // Save this neuron in the output layer 174 | output_layer.push(neuron); 175 | } 176 | 177 | // Pedantic asserts 178 | assert!(weight_id == weights.len(), 179 | "Did not use all weights"); 180 | assert!(output_layer.len() == *size, 181 | "Invalid output size for forward layer"); 182 | 183 | // Update current size 184 | cur_size = *size; 185 | } 186 | Layer::Bias { biases } => { 187 | // Apply a bias to the all neurons from the previous layer 188 | for (input, bias) in input_layer.iter().zip(biases.iter()) { 189 | output_layer.push(input + bias); 190 | } 191 | 192 | // Pedantic assert 193 | assert!(output_layer.len() == biases.len() && 194 | input_layer.len() == biases.len(), 195 | "Did not use all biases"); 196 | } 197 | Layer::LinStep => { 198 | for &input in input_layer.iter() { 199 | output_layer.push( 200 | if input > 1.0 { 201 | 1.0 202 | } else if input < -1.0 { 203 | -1.0 204 | } else { 205 | input 206 | } 207 | ) 208 | } 209 | 210 | // Pedantic assert 211 | assert!(output_layer.len() == input_layer.len(), 212 | "LinStep layer output incorrect"); 213 | } 214 | } 215 | } 216 | 217 | // Return the output layer 218 | if (self.layers.len() & 1) == 0 { 219 | &self.layer_even 220 | } else { 221 | &self.layer_odd 222 | } 223 | } 224 | 225 | /// Restore the network weights to their values prior to mutation 226 | pub fn restore(&mut self) { 227 | for &(layer_id, weight_id, prev, _new) in self.mutate_diffs.iter() { 228 | let layer = &mut self.layers[layer_id]; 229 | match layer { 230 | Layer::Forward { weights, .. } => { 231 | weights[weight_id] = prev; 232 | } 233 | Layer::Bias { biases, .. } => { 234 | biases[weight_id] = prev; 235 | } 236 | Layer::LinStep => { 237 | panic!("LinStep should never need to be restored"); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | fn find_xor_network() -> u64 { 245 | const FEEDBACK_ENABLED: bool = false; 246 | 247 | let mut network = Network::new(2); 248 | 249 | network.add_layer(Layer::Forward { size: 3, weights: Vec::new() }); 250 | network.add_layer(Layer::Bias { biases: Vec::new() }); 251 | network.add_layer(Layer::LinStep); 252 | 253 | network.add_layer(Layer::Forward { size: 5, weights: Vec::new() }); 254 | network.add_layer(Layer::Bias { biases: Vec::new() }); 255 | network.add_layer(Layer::LinStep); 256 | 257 | network.add_layer(Layer::Forward { size: 1, weights: Vec::new() }); 258 | network.add_layer(Layer::Bias { biases: Vec::new() }); 259 | network.add_layer(Layer::LinStep); 260 | 261 | /// Xor inputs 262 | const INPUT_SETS: [[f32; 2]; 4] = [ 263 | [-1., -1.], 264 | [-1., 1.], 265 | [ 1., -1.], 266 | [ 1., 1.], 267 | ]; 268 | 269 | /// All xor results 270 | const EXPECTED: [f32; 4] = [-1., 1., 1., -1.]; 271 | 272 | /// Size of the training set 273 | /// Can be reduced to change the size of the testing set, reducing the 274 | /// amount of work required to find a fitting network 275 | const TSIZE: usize = 3; 276 | 277 | // Currently known best network. Tuple is (error_rate, saved network) 278 | let mut best_network: (f32, Network) = (std::f32::MAX, network); 279 | 280 | // Create a copy of the best network as a working copy 281 | let mut network = best_network.1.clone(); 282 | 283 | for iter_id in 1u64.. { 284 | let mut error_rate = 0f32; 285 | 286 | // Modify weights randomly 287 | network.mutate_weights(); 288 | 289 | // Go through all of our training inputs 290 | for (inputs, &expected) in INPUT_SETS[..TSIZE] 291 | .iter().zip(EXPECTED[..TSIZE].iter()) { 292 | network.layer_even.clear(); 293 | network.layer_even.push(inputs[0] as f32); 294 | network.layer_even.push(inputs[1] as f32); 295 | 296 | let result = network.forward_propagate()[0]; 297 | 298 | let error = (expected - result) * (expected - result); 299 | error_rate += error; 300 | } 301 | 302 | // If this is the first result, or we improved the error rate, update 303 | // the best network 304 | if error_rate < best_network.0 { 305 | best_network = (error_rate, network.clone()); 306 | 307 | if error_rate == 0. { 308 | // Found perfect network, return iteration count until perfect 309 | // network 310 | return iter_id; 311 | } 312 | } else { 313 | if FEEDBACK_ENABLED { 314 | // Restore network to the un-mutated, known-best state 315 | network.restore(); 316 | } 317 | } 318 | } 319 | 320 | panic!("Could not find perfect network"); 321 | } 322 | 323 | fn main() { 324 | let mut fd = File::create("output.txt").unwrap(); 325 | 326 | for ii in 1u64.. { 327 | let start = Instant::now(); 328 | let iter_id = find_xor_network(); 329 | let elapsed = (Instant::now() - start) 330 | .as_nanos() as f64 / 1_000_000_000.0; 331 | 332 | if (ii & 0xff) == 0 { 333 | print!("Found xor network #{:7} in {:10} iters | \ 334 | {:10.4} | {:10.1}/sec\n", 335 | ii, iter_id, elapsed, iter_id as f64/ elapsed); 336 | } 337 | 338 | write!(fd, "{}\n", iter_id).unwrap(); 339 | fd.flush().unwrap(); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/rng.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | /// Basic xorshift-based RNG 4 | #[derive(Debug, Clone)] 5 | pub struct Rng { 6 | /// RNG seed, wrapped in a `Cell` so we can get random numbers with `&self` 7 | seed: Cell, 8 | } 9 | 10 | impl Rng { 11 | /// Create a new random number generator 12 | pub fn new() -> Self { 13 | let ret = Rng { seed: Cell::new(0) }; 14 | ret.reseed(); 15 | ret 16 | } 17 | 18 | /// Generate a new seed for this Rng 19 | pub fn reseed(&self) { 20 | let tsc = unsafe { core::arch::x86_64::_rdtsc() as u64 }; 21 | self.seed.set(tsc); 22 | 23 | // Shuffle in the TSC 24 | for _ in 0..128 { 25 | self.rand(); 26 | } 27 | } 28 | 29 | /// Using xorshift get a new random number 30 | pub fn rand(&self) -> u64 { 31 | let mut seed = self.seed.get(); 32 | seed ^= seed << 13; 33 | seed ^= seed >> 17; 34 | seed ^= seed << 43; 35 | self.seed.set(seed); 36 | seed 37 | } 38 | 39 | /// Generates a random floating point value in the range [min, max] 40 | pub fn rand_f32(&self, min: f32, max: f32) -> f32 { 41 | // Make sure max is larger than min 42 | assert!(max > min, "Invalid rand_f32 range"); 43 | 44 | // Compute the magnitude of the range 45 | let magnitude = max - min; 46 | 47 | // Generate a random value in the range [0.0, 1.0] 48 | let rand_val = (self.rand() as f32) / (std::u64::MAX as f32); 49 | 50 | // Apply the magnitude to the random value, based on the size of our 51 | // range 52 | let rand_val = rand_val * magnitude; 53 | 54 | // Skew the random value WRT to the minimum value 55 | min + rand_val 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /verify.plt: -------------------------------------------------------------------------------- 1 | clear 2 | reset 3 | set border 3 4 | 5 | set terminal wxt size 506,253 6 | 7 | set xrange [0:2500] 8 | 9 | # Add a vertical dotted line at x=0 to show centre (mean) of distribution. 10 | set yzeroaxis 11 | 12 | # Each bar is half the (visual) width of its x-range. 13 | set boxwidth 5 absolute 14 | set style fill solid 1.0 noborder 15 | 16 | set xlabel "Number of mutations until 0 error rate" 17 | set ylabel "Frequency" 18 | 19 | bin_width = 10; 20 | 21 | bin_number(x) = floor(x/bin_width) 22 | 23 | rounded(x) = bin_width * ( bin_number(x) + 0.5 ) 24 | 25 | plot 'output_nofeedback.txt' using (rounded($1)):(1) smooth frequency with boxes t "No feedback", \ 26 | 'output_feedback.txt' using (rounded($1)):(1) smooth frequency with boxes t "Feedback" 27 | --------------------------------------------------------------------------------