@@ -380,7 +380,10 @@ where
380380 T : CommandConfigurator < Child > + HasTimeout + Debug ,
381381 OT : ObserversTuple < I , S > ,
382382{
383- fn execute_input_with_command < TB : ToTargetBytesConverter < I , S > > (
383+ /// Use this when wrapping with `WithObservers` - build `CommandExecutor` with
384+ /// empty observers `()`, wrap with `WithObservers`, and let the fuzzer manage
385+ /// the observer lifecycle.
386+ fn execute_command_only < TB : ToTargetBytesConverter < I , S > > (
384387 & mut self ,
385388 target_bytes_converter : & mut TB ,
386389 state : & mut S ,
@@ -427,6 +430,19 @@ where
427430 self . observers_mut ( ) . index_mut ( & stdout_handle) . observe ( buf) ;
428431 }
429432
433+ Ok ( exit_kind)
434+ }
435+
436+ fn execute_input_with_command < TB : ToTargetBytesConverter < I , S > > (
437+ & mut self ,
438+ target_bytes_converter : & mut TB ,
439+ state : & mut S ,
440+ input : & I ,
441+ ) -> Result < ExitKind , Error > {
442+ self . observers_mut ( ) . pre_exec_all ( state, input) ?;
443+
444+ let exit_kind = self . execute_command_only ( target_bytes_converter, state, input) ?;
445+
430446 self . observers_mut ( )
431447 . post_exec_child_all ( state, input, & exit_kind) ?;
432448 Ok ( exit_kind)
@@ -955,4 +971,99 @@ mod tests {
955971
956972 assert ! ( executor. observers. 0 . output. is_some( ) ) ;
957973 }
974+
975+ /// Test that WithObservers correctly calls pre_exec and post_exec on an observer list (tuple)
976+ #[ test]
977+ #[ cfg_attr( miri, ignore) ]
978+ fn test_with_observers ( ) {
979+ use libafl_bolts:: tuples:: { Handled , MatchNameRef } ;
980+ use tuple_list:: tuple_list;
981+
982+ use crate :: {
983+ executors:: { HasObservers , WithObservers } ,
984+ observers:: TimeObserver ,
985+ } ;
986+
987+ let mut mgr: SimpleEventManager < NopInput , _ , NopState < NopInput > > =
988+ SimpleEventManager :: new ( SimpleMonitor :: new ( |status| {
989+ log:: info!( "{status}" ) ;
990+ } ) ) ;
991+
992+ // Create CommandExecutor with EMPTY observers
993+ #[ cfg( windows) ]
994+ let executor = CommandExecutor :: builder ( )
995+ . program ( "cmd" )
996+ . arg ( "/c" )
997+ . arg ( "echo" )
998+ . input ( InputLocation :: Arg { argnum : 2 } ) ;
999+ #[ cfg( not( windows) ) ]
1000+ let executor = CommandExecutor :: builder ( )
1001+ . program ( "echo" )
1002+ . input ( InputLocation :: Arg { argnum : 0 } ) ;
1003+
1004+ let executor = executor. build :: < BytesInput , ( ) , _ > ( ( ) ) . unwrap ( ) ;
1005+
1006+ // Assert that the CommandExecutor has NO observers (empty tuple)
1007+ {
1008+ use crate :: executors:: HasObservers ;
1009+ let inner_observers: & ( ) = & * executor. observers ( ) ;
1010+ assert_eq ! (
1011+ inner_observers,
1012+ & ( ) ,
1013+ "CommandExecutor should have empty observers when used with WithObservers"
1014+ ) ;
1015+ }
1016+
1017+ let time_observer1 = TimeObserver :: new ( "time1" ) ;
1018+ let time_observer2 = TimeObserver :: new ( "time2" ) ;
1019+ let handle1 = time_observer1. handle ( ) ;
1020+ let handle2 = time_observer2. handle ( ) ;
1021+
1022+ // Verify observers start with no recorded runtime (pre_exec not yet called)
1023+ assert ! (
1024+ time_observer1. last_runtime( ) . is_none( ) ,
1025+ "Observer 1 should have no runtime before execution"
1026+ ) ;
1027+ assert ! (
1028+ time_observer2. last_runtime( ) . is_none( ) ,
1029+ "Observer 2 should have no runtime before execution"
1030+ ) ;
1031+
1032+ let observer_list = tuple_list ! ( time_observer1, time_observer2) ;
1033+ let mut executor = WithObservers :: new ( executor, observer_list) ;
1034+
1035+ let mut fuzzer: NopFuzzer = NopFuzzer :: new ( ) ;
1036+ let mut state = NopState :: < NopInput > :: new ( ) ;
1037+ executor
1038+ . run_target (
1039+ & mut fuzzer,
1040+ & mut state,
1041+ & mut mgr,
1042+ & BytesInput :: new ( b"test" . to_vec ( ) ) ,
1043+ )
1044+ . unwrap ( ) ;
1045+
1046+ // Verify BOTH observers had pre_exec and post_exec called
1047+ let observers = executor. observers ( ) ;
1048+
1049+ let time_obs1: & TimeObserver = observers. get ( & handle1) . as_ref ( ) . unwrap ( ) ;
1050+ assert ! (
1051+ time_obs1. last_runtime( ) . is_some( ) ,
1052+ "Observer 1: pre_exec and post_exec should have been called (last_runtime is set)"
1053+ ) ;
1054+
1055+ let time_obs2: & TimeObserver = observers. get ( & handle2) . as_ref ( ) . unwrap ( ) ;
1056+ assert ! (
1057+ time_obs2. last_runtime( ) . is_some( ) ,
1058+ "Observer 2: pre_exec and post_exec should have been called (last_runtime is set)"
1059+ ) ;
1060+
1061+ // Both observers should have recorded similar runtimes (same execution)
1062+ let runtime1 = time_obs1. last_runtime ( ) . unwrap ( ) ;
1063+ let runtime2 = time_obs2. last_runtime ( ) . unwrap ( ) ;
1064+ assert ! (
1065+ runtime1. as_micros( ) > 0 || runtime2. as_micros( ) > 0 ,
1066+ "At least one observer should have recorded non-zero runtime"
1067+ ) ;
1068+ }
9581069}
0 commit comments