@@ -147,6 +147,17 @@ def register_fuzzing_tools(registry: ToolRegistry) -> None:
147147 "that need environment variable setup before execution."
148148 ),
149149 },
150+ "desock" : {
151+ "type" : "boolean" ,
152+ "description" : (
153+ "Enable desocketing for network daemon binaries. When true, "
154+ "intercepts socket/bind/listen/accept calls and redirects "
155+ "network I/O to stdin/stdout, allowing AFL++ to fuzz daemon "
156+ "binaries that normally read from network connections. Use "
157+ "this for binaries identified as 'network' strategy by "
158+ "analyze_fuzzing_target."
159+ ),
160+ },
150161 },
151162 "required" : ["binary_path" ],
152163 },
@@ -254,6 +265,29 @@ def register_fuzzing_tools(registry: ToolRegistry) -> None:
254265 handler = _handle_triage_crash ,
255266 )
256267
268+ registry .register (
269+ name = "diagnose_fuzzing_campaign" ,
270+ description = (
271+ "Diagnose a fuzzing campaign that may not be performing well. "
272+ "Checks campaign status, reads AFL++ logs for startup errors, "
273+ "analyzes coverage for stalls, and provides actionable recommendations "
274+ "(e.g., enable desock for network daemons, increase timeout for hangs). "
275+ "Use this when coverage is flat, execs/sec is zero, or the campaign "
276+ "seems stuck."
277+ ),
278+ input_schema = {
279+ "type" : "object" ,
280+ "properties" : {
281+ "campaign_id" : {
282+ "type" : "string" ,
283+ "description" : "The campaign ID to diagnose" ,
284+ },
285+ },
286+ "required" : ["campaign_id" ],
287+ },
288+ handler = _handle_diagnose_campaign ,
289+ )
290+
257291
258292# ---------------------------------------------------------------------------
259293# Tool handlers
@@ -555,32 +589,29 @@ async def _handle_generate_harness(input: dict, context: ToolContext) -> str:
555589 "harness_script" : harness ,
556590 }
557591 else :
558- lines .append ("Strategy: NETWORK fuzzing (daemon-style — limited support )" )
592+ lines .append ("Strategy: NETWORK fuzzing (daemon-style with desock )" )
559593 lines .append ("" )
560594 lines .append (
561- "This binary appears to be a network daemon (uses socket/bind/listen/accept). "
562- "Direct daemon fuzzing is NOT supported in QEMU user mode because the "
563- "binary forks and listens on a socket rather than reading from stdin."
595+ "This binary is a network daemon (uses socket/bind/listen/accept). "
596+ "The desock library will intercept socket calls and redirect network "
597+ "I/O to stdin/stdout, allowing AFL++ to fuzz the network parsing "
598+ "code directly."
564599 )
565600 lines .append ("" )
566- lines .append ("Alternative approaches:" )
567- lines .append (" 1. **Stdin redirection**: If the binary has a CLI/debug mode that " )
568- lines .append (" reads from stdin, use stdin strategy instead." )
569- lines .append (" 2. **LD_PRELOAD desocketing**: Use a preload library (e.g., " )
570- lines .append (" preeny's desock.so) to redirect socket I/O to stdin/stdout. " )
571- lines .append (" Set environment: {\" LD_PRELOAD\" : \" /path/to/desock.so\" }" )
572- lines .append (" 3. **CGI handler fuzzing**: If the daemon delegates to CGI handlers, " )
573- lines .append (" fuzz those handlers directly with the CGI harness approach." )
574- lines .append (" 4. **Function-level fuzzing**: Write a custom harness that calls " )
575- lines .append (" specific parsing functions from the binary's shared libraries." )
576- lines .append ("" )
577- lines .append ("If you want to attempt stdin redirection anyway:" )
601+ lines .append ("Recommended start_fuzzing_campaign parameters:" )
578602 lines .append (f' binary_path: "{ binary_path } "' )
579- lines .append (" (AFL++ will pipe fuzz input to stdin — may not reach network code)" )
603+ lines .append (" desock: true" )
604+ lines .append (" (AFL++ will pipe fuzz data through the desocketed accept() connection)" )
605+ lines .append ("" )
606+ lines .append ("Notes:" )
607+ lines .append ("- The daemon's accept() loop will receive one connection with AFL++ fuzz data" )
608+ lines .append ("- Some daemons may need arguments to skip daemonization (e.g., -f for foreground)" )
609+ lines .append ("- If coverage is flat, the daemon may fork after accept() — try adding" )
610+ lines .append (' environment: {"AFL_NO_FORKSRV": "1"}' )
580611
581612 campaign_params = {
582613 "binary_path" : binary_path ,
583- "_note " : "Daemon-style binary — stdin fuzzing may have limited coverage" ,
614+ "desock " : True ,
584615 }
585616
586617 lines .append ("" )
@@ -688,6 +719,8 @@ async def _handle_start_campaign(input: dict, context: ToolContext) -> str:
688719 config ["environment" ] = input ["environment" ]
689720 if "harness_script" in input :
690721 config ["harness_script" ] = input ["harness_script" ]
722+ if "desock" in input :
723+ config ["desock" ] = input ["desock" ]
691724
692725 svc = FuzzingService (context .db )
693726 try :
@@ -839,3 +872,211 @@ async def _handle_triage_crash(input: dict, context: ToolContext) -> str:
839872 )
840873
841874 return "\n " .join (lines )
875+
876+
877+ async def _handle_diagnose_campaign (input : dict , context : ToolContext ) -> str :
878+ """Diagnose a fuzzing campaign for performance issues."""
879+ campaign_id = input .get ("campaign_id" )
880+ if not campaign_id :
881+ return "Error: campaign_id is required."
882+
883+ from uuid import UUID
884+ import docker
885+ import docker .errors
886+
887+ svc = FuzzingService (context .db )
888+ try :
889+ campaign = await svc .get_campaign_status (UUID (campaign_id ), context .project_id )
890+ except ValueError as exc :
891+ return f"Error: { exc } "
892+
893+ config = campaign .config or {}
894+ stats = campaign .stats or {}
895+ lines : list [str ] = [
896+ f"Fuzzing Campaign Diagnostics: { campaign .id } " ,
897+ f" Binary: { campaign .binary_path } " ,
898+ f" Status: { campaign .status } " ,
899+ f" Desock: { 'enabled' if config .get ('desock' ) else 'disabled' } " ,
900+ "" ,
901+ ]
902+
903+ issues : list [str ] = []
904+ recommendations : list [str ] = []
905+
906+ # --- Check 1: Campaign status ---
907+ if campaign .status == "error" :
908+ lines .append (f" ERROR: { campaign .error_message or 'unknown error' } " )
909+ issues .append ("Campaign is in error state" )
910+ recommendations .append (
911+ "Check the error message above. The campaign may need to be "
912+ "recreated with different parameters."
913+ )
914+
915+ if campaign .status in ("stopped" , "completed" ):
916+ lines .append (" Campaign is not running." )
917+ issues .append ("Campaign is stopped — no live data available" )
918+
919+ # --- Check 2: AFL++ log from container ---
920+ afl_log = ""
921+ afl_process_running = False
922+ if campaign .container_id and campaign .status == "running" :
923+ try :
924+ client = docker .from_env ()
925+ container = client .containers .get (campaign .container_id )
926+
927+ # Read AFL++ log
928+ log_result = container .exec_run (
929+ ["tail" , "-100" , "/opt/fuzzing/afl.log" ]
930+ )
931+ if log_result .exit_code == 0 :
932+ afl_log = log_result .output .decode ("utf-8" , errors = "replace" )
933+
934+ # Check if AFL++ process is running
935+ ps_result = container .exec_run (["sh" , "-c" , "pgrep -f afl-fuzz" ])
936+ afl_process_running = ps_result .exit_code == 0
937+
938+ # Check if QEMU trace process is running
939+ qemu_result = container .exec_run (
940+ ["sh" , "-c" , "pgrep -f afl-qemu-trace" ]
941+ )
942+ qemu_running = qemu_result .exit_code == 0
943+
944+ lines .append (" Container: running" )
945+ lines .append (
946+ f" AFL++ process: { 'running' if afl_process_running else 'NOT running' } "
947+ )
948+ lines .append (
949+ f" QEMU trace: { 'running' if qemu_running else 'NOT running' } "
950+ )
951+
952+ if not afl_process_running :
953+ issues .append ("AFL++ process is not running in the container" )
954+ recommendations .append (
955+ "AFL++ crashed or failed to start. Check the log output below "
956+ "for details. You may need to stop and restart with different settings."
957+ )
958+
959+ except docker .errors .NotFound :
960+ lines .append (" Container: NOT FOUND (may have been removed)" )
961+ issues .append ("Container no longer exists" )
962+ except Exception as exc :
963+ lines .append (f" Container check failed: { exc } " )
964+
965+ # --- Check 3: Coverage analysis ---
966+ total_execs = stats .get ("total_execs" , 0 )
967+ execs_per_sec = stats .get ("execs_per_sec" , 0 )
968+ bitmap_cvg = stats .get ("bitmap_cvg" , "N/A" )
969+ saved_crashes = stats .get ("saved_crashes" , 0 )
970+ saved_hangs = stats .get ("saved_hangs" , 0 )
971+ corpus_count = stats .get ("corpus_count" , 0 )
972+
973+ if stats :
974+ lines .append ("" )
975+ lines .append (" Live statistics:" )
976+ lines .append (f" Execs/sec: { execs_per_sec } " )
977+ lines .append (f" Total execs: { total_execs } " )
978+ lines .append (f" Coverage: { bitmap_cvg } " )
979+ lines .append (f" Corpus: { corpus_count } " )
980+ lines .append (f" Crashes: { saved_crashes } " )
981+ lines .append (f" Hangs: { saved_hangs } " )
982+
983+ # Zero executions
984+ if campaign .status == "running" and total_execs == 0 :
985+ issues .append ("Zero executions — AFL++ may have failed to start" )
986+ recommendations .append (
987+ "AFL++ produced no executions. Check the log below for startup errors. "
988+ "Common causes: binary not found, missing shared libraries (check "
989+ "QEMU_LD_PREFIX), or architecture mismatch."
990+ )
991+
992+ # Flat coverage detection (100 execs is enough to detect a problem)
993+ if total_execs > 100 :
994+ cvg_str = str (bitmap_cvg ).replace ("%" , "" ).strip ()
995+ try :
996+ cvg_pct = float (cvg_str )
997+ if cvg_pct < 5.0 :
998+ issues .append (
999+ f"Very low coverage ({ bitmap_cvg } ) after { total_execs } executions"
1000+ )
1001+ if not config .get ("desock" ):
1002+ # Check if this might be a network binary
1003+ fw_result = await context .db .execute (
1004+ select (Firmware ).where (Firmware .id == campaign .firmware_id )
1005+ )
1006+ firmware = fw_result .scalar_one_or_none ()
1007+ if firmware :
1008+ try :
1009+ analysis = await svc .analyze_target (
1010+ firmware , campaign .binary_path
1011+ )
1012+ if analysis .get ("recommended_strategy" ) == "network" :
1013+ recommendations .append (
1014+ "This binary is a NETWORK DAEMON but desock is disabled. "
1015+ "AFL++ fuzz data never reaches the network parsing code. "
1016+ "ACTION: Stop this campaign and restart with desock: true"
1017+ )
1018+ except Exception :
1019+ pass
1020+ if not recommendations :
1021+ recommendations .append (
1022+ "Coverage is very low. The binary may not be processing "
1023+ "AFL++ input effectively. Consider enabling desock (if it's "
1024+ "a network daemon) or using a harness script."
1025+ )
1026+ else :
1027+ recommendations .append (
1028+ "Coverage is very low even with desock. The daemon may fork "
1029+ "after accept() — try environment: {\" AFL_NO_FORKSRV\" : \" 1\" }. "
1030+ "Or the binary may exit before reading stdin."
1031+ )
1032+ except (ValueError , TypeError ):
1033+ pass
1034+
1035+ # High hang count
1036+ if saved_hangs > 10 and saved_hangs > saved_crashes :
1037+ issues .append (f"High hang count ({ saved_hangs } ) — binary may be timing out" )
1038+ recommendations .append (
1039+ f"Many hangs detected. Current timeout: "
1040+ f"{ config .get ('timeout_per_exec' , 1000 )} ms. "
1041+ "Try increasing timeout_per_exec (e.g., 5000 or 10000)."
1042+ )
1043+
1044+ # --- Check 4: AFL++ log output ---
1045+ if afl_log :
1046+ # Look for known error patterns
1047+ if "PROGRAM ABORT" in afl_log :
1048+ issues .append ("AFL++ aborted — see log for details" )
1049+ if "No instrumentation detected" in afl_log :
1050+ issues .append ("No instrumentation — QEMU mode may not be working" )
1051+ recommendations .append (
1052+ "AFL++ reports no instrumentation. Ensure the binary architecture "
1053+ "matches the QEMU trace binary."
1054+ )
1055+ if "can't find" in afl_log .lower () or "not found" in afl_log .lower ():
1056+ issues .append ("Binary or dependency not found" )
1057+ recommendations .append (
1058+ "A file was not found. Check that binary_path is correct and "
1059+ "all shared libraries exist under the firmware root."
1060+ )
1061+
1062+ lines .append ("" )
1063+ lines .append (" AFL++ log (last lines):" )
1064+ for log_line in afl_log .strip ().split ("\n " )[- 30 :]:
1065+ lines .append (f" { log_line } " )
1066+
1067+ # --- Summary ---
1068+ lines .append ("" )
1069+ if issues :
1070+ lines .append ("ISSUES FOUND:" )
1071+ for i , issue in enumerate (issues , 1 ):
1072+ lines .append (f" { i } . { issue } " )
1073+ else :
1074+ lines .append ("No issues detected — campaign appears healthy." )
1075+
1076+ if recommendations :
1077+ lines .append ("" )
1078+ lines .append ("RECOMMENDATIONS:" )
1079+ for i , rec in enumerate (recommendations , 1 ):
1080+ lines .append (f" { i } . { rec } " )
1081+
1082+ return "\n " .join (lines )
0 commit comments