dff-py 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dff_py-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: dff-py
3
+ Version: 0.1.0
4
+ Summary: A simple differential fuzzing framework
5
+ Author-email: Justin Traglia <jtraglia@pm.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jtraglia/dff
8
+ Project-URL: Repository, https://github.com/jtraglia/dff
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+
12
+ # DFF Python Implementation
13
+
14
+ A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
15
+
16
+ ## Installation
17
+
18
+ ### From PyPI (once published)
19
+
20
+ ```bash
21
+ pip install dff-py
22
+ ```
23
+
24
+ ### From Source
25
+
26
+ ```bash
27
+ cd python
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.8 or higher
34
+ - Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
35
+ - System configured for 100 MiB shared memory segments (see main README)
36
+
37
+ ## Usage
38
+
39
+ ### Client
40
+
41
+ ```python
42
+ from dff import Client
43
+
44
+ def process_func(method: str, inputs: list[bytes]) -> bytes:
45
+ """Process function that handles fuzzing inputs."""
46
+ if method != "sha":
47
+ raise ValueError(f"Unknown method: {method}")
48
+
49
+ # Process the first input (matching Go/Java behavior)
50
+ import hashlib
51
+ return hashlib.sha256(inputs[0]).digest()
52
+
53
+ # Create and run client
54
+ client = Client("python", process_func)
55
+ client.connect()
56
+ client.run()
57
+ ```
58
+
59
+ ### Server
60
+
61
+ ```python
62
+ from dff import Server
63
+
64
+ def provider() -> list[bytes]:
65
+ """Generate fuzzing inputs."""
66
+ import random
67
+ size = random.randint(1024, 4096)
68
+ data = bytes(random.randint(0, 255) for _ in range(size))
69
+ return [data]
70
+
71
+ # Create and run server
72
+ server = Server("sha")
73
+ server.run(provider)
74
+ ```
75
+
76
+ ## Examples
77
+
78
+ See the `examples/python/` directory for complete working examples:
79
+
80
+ - `client.py` - SHA256 hashing client implementation
81
+ - `server.py` - Fuzzing server with random data provider
82
+
83
+ ### Running the Examples
84
+
85
+ Start the server:
86
+ ```bash
87
+ ./examples/python/server.py
88
+ ```
89
+
90
+ In another terminal, start one or more clients:
91
+ ```bash
92
+ ./examples/python/client.py
93
+ ./examples/python/client.py python2
94
+ ./examples/golang/client/client golang
95
+ ```
96
+
97
+ The server will detect any differences in the outputs from different clients.
98
+
99
+ ## Architecture
100
+
101
+ The framework uses:
102
+ - **Unix domain sockets** for control messages and coordination
103
+ - **System V shared memory** for efficient data transfer
104
+ - **Multiple client support** for differential testing
105
+
106
+ ### Protocol
107
+
108
+ 1. Client connects to server via Unix socket at `/tmp/dff`
109
+ 2. Client sends its name
110
+ 3. Server responds with:
111
+ - Input shared memory ID (4 bytes, big-endian)
112
+ - Output shared memory ID (4 bytes, big-endian)
113
+ - Method name (up to 64 bytes)
114
+ 4. For each fuzzing iteration:
115
+ - Server writes input data to shared memory
116
+ - Server sends message with input count and sizes
117
+ - Client processes data and writes result to output shared memory
118
+ - Client sends result size back to server
119
+ - Server compares results across clients
120
+
121
+ ## Performance
122
+
123
+ The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
124
+ - Python's Global Interpreter Lock (GIL)
125
+ - Interpreter overhead
126
+ - Dynamic typing
127
+
128
+ For better performance, consider:
129
+ - Using PyPy instead of CPython
130
+ - Implementing compute-heavy processing in C extensions
131
+ - Running multiple client instances
132
+
133
+ ## Development
134
+
135
+ ### Running Tests
136
+
137
+ ```bash
138
+ cd python
139
+ pip install -e .[dev]
140
+ pytest
141
+ ```
142
+
143
+ ### Code Quality
144
+
145
+ ```bash
146
+ # Format code
147
+ black dff/
148
+
149
+ # Lint
150
+ ruff dff/
151
+
152
+ # Type checking
153
+ mypy dff/
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT License - see the LICENSE file in the root directory.
dff_py-0.1.0/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # DFF Python Implementation
2
+
3
+ A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
4
+
5
+ ## Installation
6
+
7
+ ### From PyPI (once published)
8
+
9
+ ```bash
10
+ pip install dff-py
11
+ ```
12
+
13
+ ### From Source
14
+
15
+ ```bash
16
+ cd python
17
+ pip install -e .
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - Python 3.8 or higher
23
+ - Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
24
+ - System configured for 100 MiB shared memory segments (see main README)
25
+
26
+ ## Usage
27
+
28
+ ### Client
29
+
30
+ ```python
31
+ from dff import Client
32
+
33
+ def process_func(method: str, inputs: list[bytes]) -> bytes:
34
+ """Process function that handles fuzzing inputs."""
35
+ if method != "sha":
36
+ raise ValueError(f"Unknown method: {method}")
37
+
38
+ # Process the first input (matching Go/Java behavior)
39
+ import hashlib
40
+ return hashlib.sha256(inputs[0]).digest()
41
+
42
+ # Create and run client
43
+ client = Client("python", process_func)
44
+ client.connect()
45
+ client.run()
46
+ ```
47
+
48
+ ### Server
49
+
50
+ ```python
51
+ from dff import Server
52
+
53
+ def provider() -> list[bytes]:
54
+ """Generate fuzzing inputs."""
55
+ import random
56
+ size = random.randint(1024, 4096)
57
+ data = bytes(random.randint(0, 255) for _ in range(size))
58
+ return [data]
59
+
60
+ # Create and run server
61
+ server = Server("sha")
62
+ server.run(provider)
63
+ ```
64
+
65
+ ## Examples
66
+
67
+ See the `examples/python/` directory for complete working examples:
68
+
69
+ - `client.py` - SHA256 hashing client implementation
70
+ - `server.py` - Fuzzing server with random data provider
71
+
72
+ ### Running the Examples
73
+
74
+ Start the server:
75
+ ```bash
76
+ ./examples/python/server.py
77
+ ```
78
+
79
+ In another terminal, start one or more clients:
80
+ ```bash
81
+ ./examples/python/client.py
82
+ ./examples/python/client.py python2
83
+ ./examples/golang/client/client golang
84
+ ```
85
+
86
+ The server will detect any differences in the outputs from different clients.
87
+
88
+ ## Architecture
89
+
90
+ The framework uses:
91
+ - **Unix domain sockets** for control messages and coordination
92
+ - **System V shared memory** for efficient data transfer
93
+ - **Multiple client support** for differential testing
94
+
95
+ ### Protocol
96
+
97
+ 1. Client connects to server via Unix socket at `/tmp/dff`
98
+ 2. Client sends its name
99
+ 3. Server responds with:
100
+ - Input shared memory ID (4 bytes, big-endian)
101
+ - Output shared memory ID (4 bytes, big-endian)
102
+ - Method name (up to 64 bytes)
103
+ 4. For each fuzzing iteration:
104
+ - Server writes input data to shared memory
105
+ - Server sends message with input count and sizes
106
+ - Client processes data and writes result to output shared memory
107
+ - Client sends result size back to server
108
+ - Server compares results across clients
109
+
110
+ ## Performance
111
+
112
+ The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
113
+ - Python's Global Interpreter Lock (GIL)
114
+ - Interpreter overhead
115
+ - Dynamic typing
116
+
117
+ For better performance, consider:
118
+ - Using PyPy instead of CPython
119
+ - Implementing compute-heavy processing in C extensions
120
+ - Running multiple client instances
121
+
122
+ ## Development
123
+
124
+ ### Running Tests
125
+
126
+ ```bash
127
+ cd python
128
+ pip install -e .[dev]
129
+ pytest
130
+ ```
131
+
132
+ ### Code Quality
133
+
134
+ ```bash
135
+ # Format code
136
+ black dff/
137
+
138
+ # Lint
139
+ ruff dff/
140
+
141
+ # Type checking
142
+ mypy dff/
143
+ ```
144
+
145
+ ## License
146
+
147
+ MIT License - see the LICENSE file in the root directory.
@@ -0,0 +1,7 @@
1
+ """DFF - Differential Fuzzing Framework."""
2
+
3
+ from .client import Client
4
+ from .server import Server
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["Client", "Server"]
@@ -0,0 +1,211 @@
1
+ """DFF Client implementation."""
2
+
3
+ import ctypes
4
+ import socket
5
+ import struct
6
+ import signal
7
+ import time
8
+ from typing import Callable, List, Optional
9
+
10
+ from .shm import SharedMemory
11
+
12
+ SOCKET_PATH = "/tmp/dff"
13
+ MAX_METHOD_LENGTH = 64
14
+
15
+
16
+ ProcessFunc = Callable[[str, List[bytes]], bytes]
17
+
18
+
19
+ class Client:
20
+ """Client encapsulates the client-side behavior for connecting to the fuzzing server."""
21
+
22
+ def __init__(self, name: str, process_func: ProcessFunc):
23
+ """Initialize a new Client.
24
+
25
+ Args:
26
+ name: The client identifier sent to the server
27
+ process_func: The callback function used to process fuzzing inputs
28
+ """
29
+ self.name = name
30
+ self.process_func = process_func
31
+ self.conn: Optional[socket.socket] = None
32
+ self.input_shm: Optional[ctypes.Array] = None
33
+ self.output_shm: Optional[ctypes.Array] = None
34
+ self.input_shm_obj: Optional[SharedMemory] = None
35
+ self.output_shm_obj: Optional[SharedMemory] = None
36
+ self.method: str = ""
37
+ self.shutdown = False
38
+
39
+ def connect(self) -> None:
40
+ """Establish a connection to the fuzzing server.
41
+
42
+ Connects to the server, sends the client name, attaches to shared memory
43
+ segments, and reads the fuzzing method from the server.
44
+
45
+ Raises:
46
+ ConnectionError: If connection fails
47
+ OSError: If shared memory operations fail
48
+ """
49
+ max_retries = 10
50
+ retry_delay = 0.1
51
+
52
+ for attempt in range(max_retries):
53
+ try:
54
+ self.conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
55
+ self.conn.connect(SOCKET_PATH)
56
+ break
57
+ except (ConnectionError, FileNotFoundError) as e:
58
+ if attempt == max_retries - 1:
59
+ raise ConnectionError(
60
+ f"Failed to connect to server after {max_retries} attempts: {e}"
61
+ )
62
+ time.sleep(retry_delay)
63
+
64
+ if not self.conn:
65
+ raise ConnectionError("Failed to establish connection")
66
+
67
+ self.conn.sendall(self.name.encode())
68
+
69
+ input_shm_id_bytes = self.conn.recv(4)
70
+ if len(input_shm_id_bytes) != 4:
71
+ raise ConnectionError("Failed to read input shared memory ID")
72
+ input_shm_id = struct.unpack(">I", input_shm_id_bytes)[0]
73
+
74
+ self.input_shm_obj = SharedMemory(input_shm_id)
75
+ self.input_shm = self.input_shm_obj.attach()
76
+
77
+ output_shm_id_bytes = self.conn.recv(4)
78
+ if len(output_shm_id_bytes) != 4:
79
+ raise ConnectionError("Failed to read output shared memory ID")
80
+ output_shm_id = struct.unpack(">I", output_shm_id_bytes)[0]
81
+
82
+ self.output_shm_obj = SharedMemory(output_shm_id)
83
+ self.output_shm = self.output_shm_obj.attach()
84
+
85
+ method_bytes = self.conn.recv(MAX_METHOD_LENGTH)
86
+ if not method_bytes:
87
+ raise ConnectionError("Failed to read method name")
88
+ self.method = method_bytes.decode().rstrip('\x00')
89
+
90
+ print(f"Connected with fuzzing method: {self.method}")
91
+
92
+ def run(self) -> None:
93
+ """Run the client fuzzing loop.
94
+
95
+ Waits for the server to send input sizes, extracts data from shared memory,
96
+ processes it via the provided process_func, writes results to output shared
97
+ memory, and sends back the result size.
98
+
99
+ Raises:
100
+ RuntimeError: If not connected
101
+ Exception: Any exception from the process function
102
+ """
103
+ if not self.conn or not self.input_shm or not self.output_shm:
104
+ raise RuntimeError("Client not connected")
105
+
106
+ def signal_handler(_signum: int, _frame: object) -> None:
107
+ self.shutdown = True
108
+ print("\nShutting down gracefully...")
109
+
110
+ signal.signal(signal.SIGINT, signal_handler)
111
+ signal.signal(signal.SIGTERM, signal_handler)
112
+
113
+ print("Client running... Press Ctrl+C to exit.")
114
+
115
+ try:
116
+ while not self.shutdown:
117
+ # Read the message containing number of inputs and their sizes
118
+ input_msg = self.conn.recv(1024)
119
+ if not input_msg:
120
+ break
121
+
122
+ if len(input_msg) < 4:
123
+ raise ValueError(f"Invalid input message length: {len(input_msg)}")
124
+
125
+ # First 4 bytes: number of inputs
126
+ num_inputs = struct.unpack(">I", input_msg[0:4])[0]
127
+
128
+ # Following bytes: sizes of each input (4 bytes each)
129
+ expected_length = 4 + (num_inputs * 4)
130
+ if len(input_msg) < expected_length:
131
+ raise ValueError(f"Input message too short: expected {expected_length}, got {len(input_msg)}")
132
+
133
+ # Parse all sizes at once
134
+ input_sizes = struct.unpack(f">{num_inputs}I", input_msg[4:4 + num_inputs * 4])
135
+
136
+ inputs: List[bytes] = []
137
+ offset = 0
138
+ for size in input_sizes:
139
+ if offset + size > len(self.input_shm):
140
+ raise ValueError(f"Input size {size} at offset {offset} exceeds buffer")
141
+ # Use string_at to read bytes directly from memory address
142
+ data_ptr = ctypes.addressof(self.input_shm) + offset
143
+ data = ctypes.string_at(data_ptr, size)
144
+ inputs.append(data)
145
+ offset += size
146
+
147
+ try:
148
+ start_time = time.perf_counter()
149
+ result = self.process_func(self.method, inputs)
150
+ elapsed_time = (time.perf_counter() - start_time) * 1000
151
+ print(f"Processing time: {elapsed_time:.2f}ms")
152
+ except Exception as e:
153
+ print(f"Process function error: {e}")
154
+ result = b""
155
+
156
+ if not isinstance(result, bytes):
157
+ raise TypeError(f"Process function must return bytes, got {type(result)}")
158
+
159
+ if len(result) > len(self.output_shm):
160
+ raise ValueError(f"Result size {len(result)} exceeds output buffer")
161
+
162
+ # Write result bytes to output shared memory using memmove for performance
163
+ if isinstance(result, bytes) and len(result) > 0:
164
+ dest_ptr = ctypes.addressof(self.output_shm)
165
+ src = ctypes.c_char_p(result)
166
+ ctypes.memmove(dest_ptr, src, len(result))
167
+ else:
168
+ # Fallback for empty or non-bytes
169
+ self.output_shm[0:len(result)] = result
170
+
171
+ self.conn.sendall(struct.pack(">I", len(result)))
172
+
173
+ except Exception as e:
174
+ if not self.shutdown:
175
+ print(f"Client error: {e}")
176
+ raise
177
+ finally:
178
+ self.close()
179
+
180
+ def close(self) -> None:
181
+ """Close the client connection and clean up resources."""
182
+ if self.conn:
183
+ try:
184
+ self.conn.close()
185
+ except:
186
+ pass
187
+ self.conn = None
188
+
189
+ if self.input_shm_obj:
190
+ try:
191
+ self.input_shm_obj.detach()
192
+ except:
193
+ pass
194
+ self.input_shm_obj = None
195
+ self.input_shm = None
196
+
197
+ if self.output_shm_obj:
198
+ try:
199
+ self.output_shm_obj.detach()
200
+ except:
201
+ pass
202
+ self.output_shm_obj = None
203
+ self.output_shm = None
204
+
205
+ def __enter__(self) -> "Client":
206
+ """Context manager entry."""
207
+ return self
208
+
209
+ def __exit__(self, *_args: object) -> None:
210
+ """Context manager exit."""
211
+ self.close()
@@ -0,0 +1,343 @@
1
+ """DFF Server implementation."""
2
+
3
+ import ctypes
4
+ import hashlib
5
+ import os
6
+ import socket
7
+ import struct
8
+ import signal
9
+ import threading
10
+ import time
11
+ from typing import Callable, Dict, List, Optional
12
+ from collections import defaultdict
13
+
14
+ from .shm import SharedMemory, IPC_CREAT, IPC_EXCL, IPC_RMID
15
+
16
+ SOCKET_PATH = "/tmp/dff"
17
+ DEFAULT_INPUT_SHM_KEY = 1000
18
+ DEFAULT_SHM_MAX_SIZE = 100 * 1024 * 1024 # 100 MiB
19
+ DEFAULT_SHM_PERM = 0o666
20
+
21
+ ProviderFunc = Callable[[], List[bytes]]
22
+
23
+
24
+ class ClientEntry:
25
+ """Represents a connected client."""
26
+
27
+ def __init__(self, name: str, conn: socket.socket, shm_id: int, method: str):
28
+ self.name = name
29
+ self.conn = conn
30
+ self.shm_id = shm_id
31
+ self.shm_buffer = bytearray(DEFAULT_SHM_MAX_SIZE)
32
+ self.method = method
33
+
34
+
35
+ class Server:
36
+ """Server encapsulates the server-side behavior for the fuzzing framework."""
37
+
38
+ def __init__(self, method: str):
39
+ """Initialize a new Server.
40
+
41
+ Args:
42
+ method: The fuzzing method name to send to clients
43
+ """
44
+ self.method = method
45
+ self.input_shm_key = DEFAULT_INPUT_SHM_KEY
46
+ self.shm_max_size = DEFAULT_SHM_MAX_SIZE
47
+ self.shm_perm = DEFAULT_SHM_PERM
48
+ self.clients: Dict[str, ClientEntry] = {}
49
+ self.clients_lock = threading.Lock()
50
+ self.shutdown = False
51
+ self.iteration_count = 0
52
+ self.total_duration = 0.0
53
+ self.input_shm: Optional[SharedMemory] = None
54
+ self.input_shm_buffer: Optional[ctypes.Array] = None
55
+ self.listener: Optional[socket.socket] = None
56
+
57
+ def _cleanup_existing_shm(self) -> None:
58
+ """Clean up any existing shared memory with our key."""
59
+ try:
60
+ existing_shm = SharedMemory.get(self.input_shm_key)
61
+ existing_shm.remove()
62
+ print(f"Removed existing input shared memory segment with key {self.input_shm_key}")
63
+ except OSError:
64
+ pass
65
+
66
+ def _create_shared_memory(self) -> None:
67
+ """Create the input shared memory segment."""
68
+ self._cleanup_existing_shm()
69
+
70
+ self.input_shm = SharedMemory.create(
71
+ self.input_shm_key,
72
+ self.shm_max_size,
73
+ self.shm_perm
74
+ )
75
+
76
+ self.input_shm_buffer = self.input_shm.attach()
77
+
78
+ def _handle_client(self, conn: socket.socket, addr: str) -> None:
79
+ """Handle a new client connection."""
80
+ try:
81
+ # Receive client name
82
+ name_bytes = conn.recv(256)
83
+ if not name_bytes:
84
+ conn.close()
85
+ return
86
+
87
+ client_name = name_bytes.decode().rstrip('\x00')
88
+ # Create output shared memory for this client
89
+ output_shm_key = self.input_shm_key + len(self.clients) + 1
90
+
91
+ # Clean up if it exists
92
+ try:
93
+ existing = SharedMemory.get(output_shm_key)
94
+ existing.remove()
95
+ except OSError:
96
+ pass
97
+
98
+ output_shm = SharedMemory.create(
99
+ output_shm_key,
100
+ self.shm_max_size,
101
+ self.shm_perm
102
+ )
103
+
104
+ # Send input shared memory ID
105
+ conn.sendall(struct.pack(">I", self.input_shm.shmid))
106
+
107
+ # Send output shared memory ID
108
+ conn.sendall(struct.pack(">I", output_shm.shmid))
109
+
110
+ # Send method name
111
+ conn.sendall(self.method.encode())
112
+
113
+ # Store client
114
+ with self.clients_lock:
115
+ if client_name not in self.clients:
116
+ self.clients[client_name] = ClientEntry(
117
+ client_name, conn, output_shm.shmid, self.method
118
+ )
119
+ print(f"Registered new client: {client_name}")
120
+
121
+ except Exception as e:
122
+ print(f"Error handling client: {e}")
123
+ conn.close()
124
+
125
+ def _status_updates(self) -> None:
126
+ """Print status updates every 5 seconds."""
127
+ while not self.shutdown:
128
+ time.sleep(5)
129
+
130
+ if self.iteration_count > 0:
131
+ with self.clients_lock:
132
+ client_names = sorted(self.clients.keys())
133
+
134
+ avg_duration = self.total_duration / self.iteration_count
135
+ total_seconds = int(self.total_duration)
136
+ avg_ms = int(avg_duration * 1000)
137
+
138
+ print(f"Fuzzing Time: {total_seconds}s, Iterations: {self.iteration_count}, "
139
+ f"Average Iteration: {avg_ms}ms, Clients: {','.join(client_names)}")
140
+
141
+ def _accept_clients(self) -> None:
142
+ """Accept client connections in a separate thread."""
143
+ while not self.shutdown:
144
+ try:
145
+ self.listener.settimeout(1.0)
146
+ conn, addr = self.listener.accept()
147
+ # Handle each client in a separate thread
148
+ thread = threading.Thread(
149
+ target=self._handle_client,
150
+ args=(conn, addr)
151
+ )
152
+ thread.daemon = True
153
+ thread.start()
154
+ except socket.timeout:
155
+ continue
156
+ except Exception as e:
157
+ if not self.shutdown:
158
+ print(f"Error accepting client: {e}")
159
+
160
+ def run(self, provider: ProviderFunc) -> None:
161
+ """Run the fuzzing server.
162
+
163
+ Args:
164
+ provider: Function that generates fuzzing inputs
165
+ """
166
+ # Setup signal handler
167
+ def signal_handler(_signum: int, _frame: object) -> None:
168
+ self.shutdown = True
169
+ print("\nShutting down server...")
170
+
171
+ signal.signal(signal.SIGINT, signal_handler)
172
+ signal.signal(signal.SIGTERM, signal_handler)
173
+
174
+ # Clean up any existing socket
175
+ if os.path.exists(SOCKET_PATH):
176
+ os.unlink(SOCKET_PATH)
177
+
178
+ # Create shared memory
179
+ self._create_shared_memory()
180
+
181
+ # Create and bind socket
182
+ self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
183
+ self.listener.bind(SOCKET_PATH)
184
+ self.listener.listen(10)
185
+
186
+ # Start accepting clients in a separate thread
187
+ accept_thread = threading.Thread(target=self._accept_clients)
188
+ accept_thread.daemon = True
189
+ accept_thread.start()
190
+
191
+ # Start status updates thread
192
+ status_thread = threading.Thread(target=self._status_updates)
193
+ status_thread.daemon = True
194
+ status_thread.start()
195
+
196
+ # Main fuzzing loop
197
+ try:
198
+ while not self.shutdown:
199
+ # Wait until at least one client is connected
200
+ if len(self.clients) == 0:
201
+ print("Waiting for a client...")
202
+ time.sleep(1)
203
+ continue
204
+
205
+ start_time = time.perf_counter()
206
+
207
+ # Generate inputs
208
+ inputs = provider()
209
+ if not inputs:
210
+ continue
211
+
212
+ # Prepare input data
213
+ input_sizes = []
214
+ offset = 0
215
+ for input_data in inputs:
216
+ size = len(input_data)
217
+ if offset + size > self.shm_max_size:
218
+ print(f"Warning: Input data exceeds shared memory size")
219
+ break
220
+
221
+ # Write to shared memory using memmove for better performance
222
+ dest_ptr = ctypes.addressof(self.input_shm_buffer) + offset
223
+ if isinstance(input_data, bytes):
224
+ src = ctypes.c_char_p(input_data)
225
+ ctypes.memmove(dest_ptr, src, size)
226
+ else:
227
+ # Fallback for other types
228
+ self.input_shm_buffer[offset:offset + size] = input_data
229
+ input_sizes.append(size)
230
+ offset += size
231
+
232
+ # Prepare message with number of inputs and their sizes
233
+ msg = struct.pack(">I", len(input_sizes))
234
+ for size in input_sizes:
235
+ msg += struct.pack(">I", size)
236
+
237
+ # Send to all clients and collect results
238
+ results = {}
239
+ dead_clients = []
240
+
241
+ with self.clients_lock:
242
+ for name, client in self.clients.items():
243
+ try:
244
+ # Send input sizes
245
+ client.conn.sendall(msg)
246
+
247
+ # Read result size
248
+ result_size_bytes = client.conn.recv(4)
249
+ if len(result_size_bytes) != 4:
250
+ dead_clients.append(name)
251
+ continue
252
+
253
+ result_size = struct.unpack(">I", result_size_bytes)[0]
254
+
255
+ # Read result from client's output shared memory
256
+ output_shm = SharedMemory(client.shm_id)
257
+ output_buffer = output_shm.attach()
258
+ result = bytes(output_buffer[0:result_size])
259
+ output_shm.detach()
260
+
261
+ results[name] = result
262
+
263
+ except Exception as e:
264
+ print(f"Error communicating with client {name}: {e}")
265
+ dead_clients.append(name)
266
+
267
+ # Remove dead clients
268
+ for name in dead_clients:
269
+ print(f"Removing dead client: {name}")
270
+ with self.clients_lock:
271
+ if name in self.clients:
272
+ try:
273
+ self.clients[name].conn.close()
274
+ except:
275
+ pass
276
+ del self.clients[name]
277
+
278
+ # Check for differences
279
+ if len(results) > 1:
280
+ first_result = None
281
+ all_same = True
282
+ for result in results.values():
283
+ if first_result is None:
284
+ first_result = result
285
+ elif result != first_result:
286
+ all_same = False
287
+ break
288
+
289
+ if not all_same:
290
+ print("Values are different:")
291
+ for name, result in results.items():
292
+ result_hash = hashlib.sha256(result).hexdigest()
293
+ print(f"Key: {name}, Value: {result_hash}")
294
+
295
+ # Update statistics
296
+ duration = time.perf_counter() - start_time
297
+ self.iteration_count += 1
298
+ self.total_duration += duration
299
+
300
+
301
+ except Exception as e:
302
+ if not self.shutdown:
303
+ print(f"Server error: {e}")
304
+ raise
305
+ finally:
306
+ self._cleanup()
307
+
308
+ def _cleanup(self) -> None:
309
+ """Clean up resources."""
310
+ print("Cleaning up server resources...")
311
+
312
+ # Close client connections
313
+ with self.clients_lock:
314
+ for client in self.clients.values():
315
+ try:
316
+ client.conn.close()
317
+ except:
318
+ pass
319
+ self.clients.clear()
320
+
321
+ # Close listener
322
+ if self.listener:
323
+ try:
324
+ self.listener.close()
325
+ except:
326
+ pass
327
+
328
+ # Clean up socket file
329
+ if os.path.exists(SOCKET_PATH):
330
+ try:
331
+ os.unlink(SOCKET_PATH)
332
+ except:
333
+ pass
334
+
335
+ # Clean up shared memory
336
+ if self.input_shm:
337
+ try:
338
+ self.input_shm.detach()
339
+ self.input_shm.remove()
340
+ except:
341
+ pass
342
+
343
+ print("Server shutdown complete")
@@ -0,0 +1,186 @@
1
+ """System V shared memory wrapper for Python."""
2
+
3
+ import ctypes
4
+ import ctypes.util
5
+ import sys
6
+ from typing import Optional
7
+
8
+ IPC_CREAT = 0o1000
9
+ IPC_EXCL = 0o2000
10
+ IPC_RMID = 0
11
+
12
+ if sys.platform == "darwin":
13
+ libc_name = ctypes.util.find_library("c")
14
+ elif sys.platform.startswith("linux"):
15
+ libc_name = ctypes.util.find_library("c")
16
+ else:
17
+ raise OSError(f"Unsupported platform: {sys.platform}")
18
+
19
+ if not libc_name:
20
+ raise OSError("Could not find libc")
21
+
22
+ libc = ctypes.CDLL(libc_name)
23
+
24
+
25
+ class ShmidDs(ctypes.Structure):
26
+ """Shared memory segment descriptor structure."""
27
+ if sys.platform == "darwin":
28
+ _fields_ = [
29
+ ("shm_perm", ctypes.c_void_p),
30
+ ("shm_segsz", ctypes.c_size_t),
31
+ ("shm_lpid", ctypes.c_int),
32
+ ("shm_cpid", ctypes.c_int),
33
+ ("shm_nattch", ctypes.c_short),
34
+ ("shm_atime", ctypes.c_long),
35
+ ("shm_dtime", ctypes.c_long),
36
+ ("shm_ctime", ctypes.c_long),
37
+ ("shm_internal", ctypes.c_void_p),
38
+ ]
39
+ else:
40
+ _fields_ = [
41
+ ("shm_perm", ctypes.c_void_p),
42
+ ("shm_segsz", ctypes.c_size_t),
43
+ ("shm_atime", ctypes.c_long),
44
+ ("shm_dtime", ctypes.c_long),
45
+ ("shm_ctime", ctypes.c_long),
46
+ ("shm_cpid", ctypes.c_int),
47
+ ("shm_lpid", ctypes.c_int),
48
+ ("shm_nattch", ctypes.c_short),
49
+ ]
50
+
51
+
52
+ libc.shmget.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.c_int]
53
+ libc.shmget.restype = ctypes.c_int
54
+
55
+ libc.shmat.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
56
+ libc.shmat.restype = ctypes.c_void_p
57
+
58
+ libc.shmdt.argtypes = [ctypes.c_void_p]
59
+ libc.shmdt.restype = ctypes.c_int
60
+
61
+ libc.shmctl.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ShmidDs)]
62
+ libc.shmctl.restype = ctypes.c_int
63
+
64
+
65
+ class SharedMemory:
66
+ """Wrapper for System V shared memory operations."""
67
+
68
+ def __init__(self, shmid: int, size: int = 0):
69
+ """Initialize shared memory wrapper.
70
+
71
+ Args:
72
+ shmid: Shared memory ID
73
+ size: Size of the shared memory segment (for reference)
74
+ """
75
+ self.shmid = shmid
76
+ self.size = size
77
+ self.addr: Optional[int] = None
78
+ self._attached = False
79
+
80
+ @classmethod
81
+ def create(cls, key: int, size: int, perm: int = 0o666) -> "SharedMemory":
82
+ """Create a new shared memory segment.
83
+
84
+ Args:
85
+ key: IPC key for the shared memory segment
86
+ size: Size of the segment in bytes
87
+ perm: Permissions for the segment
88
+
89
+ Returns:
90
+ SharedMemory object
91
+
92
+ Raises:
93
+ OSError: If creation fails
94
+ """
95
+ flags = perm | IPC_CREAT | IPC_EXCL
96
+ shmid = libc.shmget(key, size, flags)
97
+ if shmid == -1:
98
+ raise OSError(f"Failed to create shared memory with key {key}")
99
+ return cls(shmid, size)
100
+
101
+ @classmethod
102
+ def get(cls, key: int, size: int = 0) -> "SharedMemory":
103
+ """Get existing shared memory segment.
104
+
105
+ Args:
106
+ key: IPC key for the shared memory segment
107
+ size: Expected size (0 to get existing)
108
+
109
+ Returns:
110
+ SharedMemory object
111
+
112
+ Raises:
113
+ OSError: If segment doesn't exist
114
+ """
115
+ shmid = libc.shmget(key, size, 0)
116
+ if shmid == -1:
117
+ raise OSError(f"Failed to get shared memory with key {key}")
118
+ return cls(shmid, size)
119
+
120
+ def attach(self, addr: int = 0, flags: int = 0) -> ctypes.Array:
121
+ """Attach to the shared memory segment.
122
+
123
+ Args:
124
+ addr: Preferred attach address (0 for system choice)
125
+ flags: Attach flags
126
+
127
+ Returns:
128
+ ctypes array of the attached segment
129
+
130
+ Raises:
131
+ OSError: If attach fails
132
+ """
133
+ if self._attached:
134
+ raise RuntimeError("Already attached to shared memory")
135
+
136
+ result = libc.shmat(self.shmid, addr, flags)
137
+ if result == -1:
138
+ raise OSError(f"Failed to attach to shared memory ID {self.shmid}")
139
+
140
+ self.addr = result
141
+ self._attached = True
142
+
143
+ if self.size > 0:
144
+ return (ctypes.c_ubyte * self.size).from_address(self.addr)
145
+ else:
146
+ return (ctypes.c_ubyte * (100 * 1024 * 1024)).from_address(self.addr)
147
+
148
+ def detach(self) -> None:
149
+ """Detach from the shared memory segment."""
150
+ if not self._attached or self.addr is None:
151
+ return
152
+
153
+ result = libc.shmdt(self.addr)
154
+ if result == -1:
155
+ raise OSError("Failed to detach from shared memory")
156
+
157
+ self.addr = None
158
+ self._attached = False
159
+
160
+ def remove(self) -> None:
161
+ """Remove the shared memory segment."""
162
+ result = libc.shmctl(self.shmid, IPC_RMID, None)
163
+ if result == -1:
164
+ raise OSError(f"Failed to remove shared memory ID {self.shmid}")
165
+
166
+ def __del__(self) -> None:
167
+ """Cleanup - detach if still attached."""
168
+ if self._attached:
169
+ try:
170
+ self.detach()
171
+ except:
172
+ pass
173
+
174
+
175
+ def attach_by_id(shmid: int, size: int = 0) -> ctypes.Array:
176
+ """Convenience function to attach to shared memory by ID.
177
+
178
+ Args:
179
+ shmid: Shared memory ID
180
+ size: Expected size (0 for default max)
181
+
182
+ Returns:
183
+ ctypes array of the attached segment
184
+ """
185
+ shm = SharedMemory(shmid, size)
186
+ return shm.attach()
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: dff-py
3
+ Version: 0.1.0
4
+ Summary: A simple differential fuzzing framework
5
+ Author-email: Justin Traglia <jtraglia@pm.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jtraglia/dff
8
+ Project-URL: Repository, https://github.com/jtraglia/dff
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+
12
+ # DFF Python Implementation
13
+
14
+ A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
15
+
16
+ ## Installation
17
+
18
+ ### From PyPI (once published)
19
+
20
+ ```bash
21
+ pip install dff-py
22
+ ```
23
+
24
+ ### From Source
25
+
26
+ ```bash
27
+ cd python
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ - Python 3.8 or higher
34
+ - Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
35
+ - System configured for 100 MiB shared memory segments (see main README)
36
+
37
+ ## Usage
38
+
39
+ ### Client
40
+
41
+ ```python
42
+ from dff import Client
43
+
44
+ def process_func(method: str, inputs: list[bytes]) -> bytes:
45
+ """Process function that handles fuzzing inputs."""
46
+ if method != "sha":
47
+ raise ValueError(f"Unknown method: {method}")
48
+
49
+ # Process the first input (matching Go/Java behavior)
50
+ import hashlib
51
+ return hashlib.sha256(inputs[0]).digest()
52
+
53
+ # Create and run client
54
+ client = Client("python", process_func)
55
+ client.connect()
56
+ client.run()
57
+ ```
58
+
59
+ ### Server
60
+
61
+ ```python
62
+ from dff import Server
63
+
64
+ def provider() -> list[bytes]:
65
+ """Generate fuzzing inputs."""
66
+ import random
67
+ size = random.randint(1024, 4096)
68
+ data = bytes(random.randint(0, 255) for _ in range(size))
69
+ return [data]
70
+
71
+ # Create and run server
72
+ server = Server("sha")
73
+ server.run(provider)
74
+ ```
75
+
76
+ ## Examples
77
+
78
+ See the `examples/python/` directory for complete working examples:
79
+
80
+ - `client.py` - SHA256 hashing client implementation
81
+ - `server.py` - Fuzzing server with random data provider
82
+
83
+ ### Running the Examples
84
+
85
+ Start the server:
86
+ ```bash
87
+ ./examples/python/server.py
88
+ ```
89
+
90
+ In another terminal, start one or more clients:
91
+ ```bash
92
+ ./examples/python/client.py
93
+ ./examples/python/client.py python2
94
+ ./examples/golang/client/client golang
95
+ ```
96
+
97
+ The server will detect any differences in the outputs from different clients.
98
+
99
+ ## Architecture
100
+
101
+ The framework uses:
102
+ - **Unix domain sockets** for control messages and coordination
103
+ - **System V shared memory** for efficient data transfer
104
+ - **Multiple client support** for differential testing
105
+
106
+ ### Protocol
107
+
108
+ 1. Client connects to server via Unix socket at `/tmp/dff`
109
+ 2. Client sends its name
110
+ 3. Server responds with:
111
+ - Input shared memory ID (4 bytes, big-endian)
112
+ - Output shared memory ID (4 bytes, big-endian)
113
+ - Method name (up to 64 bytes)
114
+ 4. For each fuzzing iteration:
115
+ - Server writes input data to shared memory
116
+ - Server sends message with input count and sizes
117
+ - Client processes data and writes result to output shared memory
118
+ - Client sends result size back to server
119
+ - Server compares results across clients
120
+
121
+ ## Performance
122
+
123
+ The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
124
+ - Python's Global Interpreter Lock (GIL)
125
+ - Interpreter overhead
126
+ - Dynamic typing
127
+
128
+ For better performance, consider:
129
+ - Using PyPy instead of CPython
130
+ - Implementing compute-heavy processing in C extensions
131
+ - Running multiple client instances
132
+
133
+ ## Development
134
+
135
+ ### Running Tests
136
+
137
+ ```bash
138
+ cd python
139
+ pip install -e .[dev]
140
+ pytest
141
+ ```
142
+
143
+ ### Code Quality
144
+
145
+ ```bash
146
+ # Format code
147
+ black dff/
148
+
149
+ # Lint
150
+ ruff dff/
151
+
152
+ # Type checking
153
+ mypy dff/
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT License - see the LICENSE file in the root directory.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ dff/__init__.py
4
+ dff/client.py
5
+ dff/server.py
6
+ dff/shm.py
7
+ dff_py.egg-info/PKG-INFO
8
+ dff_py.egg-info/SOURCES.txt
9
+ dff_py.egg-info/dependency_links.txt
10
+ dff_py.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ dff
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dff-py"
7
+ version = "0.1.0"
8
+ description = "A simple differential fuzzing framework"
9
+ readme = "README.md"
10
+ authors = [{name = "Justin Traglia", email = "jtraglia@pm.me"}]
11
+ license = {text = "MIT"}
12
+ requires-python = ">=3.9"
13
+ dependencies = []
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/jtraglia/dff"
17
+ Repository = "https://github.com/jtraglia/dff"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["."]
21
+ include = ["dff*"]
dff_py-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+