@@ -3841,6 +3841,7 @@ mod trace_events {
38413841 }
38423842}
38433843
3844+ // =============================================================================
38443845// =============================================================================
38453846// TYPESCRIPT / ZAPCODE SECURITY (TM-TS)
38463847//
@@ -3967,3 +3968,179 @@ mod typescript_security {
39673968 ) ;
39683969 }
39693970}
3971+
3972+ // =============================================================================
3973+ // ADVERSARIAL TESTS — SPARSE ARRAYS, EXTREME INDICES, EXPANSION BOMBS
3974+ // Inspired by zapcode's adversarial test suite (issue #934)
3975+ // =============================================================================
3976+
3977+ mod zapcode_inspired_adversarial {
3978+ use super :: * ;
3979+
3980+ /// TM-DOS-060: Huge sparse index allocation.
3981+ /// Assigning to index 999999999 must not allocate ~1B empty slots.
3982+ /// bashkit uses HashMap internally, so the storage is O(1) per entry,
3983+ /// but the max_array_entries limit should still cap total entries.
3984+ #[ tokio:: test]
3985+ async fn sparse_array_huge_index ( ) {
3986+ let mem = MemoryLimits :: new ( ) . max_array_entries ( 100 ) ;
3987+ let limits = ExecutionLimits :: new ( ) . max_commands ( 1_000 ) ;
3988+ let mut bash = Bash :: builder ( )
3989+ . limits ( limits)
3990+ . memory_limits ( mem)
3991+ . session_limits ( SessionLimits :: unlimited ( ) )
3992+ . build ( ) ;
3993+
3994+ let result = bash
3995+ . exec ( "declare -a arr; arr[999999999]=x; echo ${#arr[@]}" )
3996+ . await
3997+ . unwrap ( ) ;
3998+ // Should succeed (HashMap-based, only 1 entry) or be capped — no OOM
3999+ assert_eq ! ( result. exit_code, 0 ) ;
4000+ // The array has at most 1 entry — certainly not ~1B
4001+ let count: usize = result. stdout . trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
4002+ assert ! (
4003+ count <= 100 ,
4004+ "Sparse index must not cause mass allocation, got count={}" ,
4005+ count
4006+ ) ;
4007+ }
4008+
4009+ /// TM-DOS-060: Extreme negative array index.
4010+ /// Must not panic, no OOB memory access, no memory corruption.
4011+ /// The key security property is: no panic, no crash, no unbounded allocation.
4012+ /// Bash wraps negative indices modulo array length, so some element may be returned.
4013+ #[ tokio:: test]
4014+ async fn sparse_array_extreme_negative_index ( ) {
4015+ let limits = ExecutionLimits :: new ( ) . max_commands ( 1_000 ) ;
4016+ let mut bash = Bash :: builder ( ) . limits ( limits) . build ( ) ;
4017+
4018+ let result = bash
4019+ . exec ( "declare -a arr=(a b c); echo \" ${arr[-999999999]}\" " )
4020+ . await
4021+ . unwrap ( ) ;
4022+ // Security property: no panic, no crash, graceful completion
4023+ assert_eq ! ( result. exit_code, 0 ) ;
4024+ // Output should be either empty or one of the valid elements (wrapping is acceptable)
4025+ let out = result. stdout . trim ( ) ;
4026+ assert ! (
4027+ out. is_empty( ) || [ "a" , "b" , "c" ] . contains( & out) ,
4028+ "Extreme negative index should return empty or valid element, got: {:?}" ,
4029+ out
4030+ ) ;
4031+ }
4032+
4033+ /// TM-DOS-060: Array entry exhaustion under load.
4034+ /// Populating 200K entries via loop must be stopped by max_array_entries (100K default)
4035+ /// or by the loop iteration limit — whichever fires first.
4036+ #[ tokio:: test]
4037+ async fn array_entry_exhaustion_under_load ( ) {
4038+ let mem = MemoryLimits :: new ( ) . max_array_entries ( 100 ) ;
4039+ let limits = ExecutionLimits :: new ( )
4040+ . max_commands ( 500_000 )
4041+ . max_loop_iterations ( 500_000 )
4042+ . max_total_loop_iterations ( 500_000 ) ;
4043+ let mut bash = Bash :: builder ( )
4044+ . limits ( limits)
4045+ . memory_limits ( mem)
4046+ . session_limits ( SessionLimits :: unlimited ( ) )
4047+ . build ( ) ;
4048+
4049+ let script = r#"
4050+ declare -a arr
4051+ i=0
4052+ while [ $i -lt 200 ]; do
4053+ arr[$i]=x
4054+ i=$((i+1))
4055+ done
4056+ echo ${#arr[@]}
4057+ "# ;
4058+ let result = bash. exec ( script) . await . unwrap ( ) ;
4059+ assert_eq ! ( result. exit_code, 0 ) ;
4060+ let count: usize = result. stdout . trim ( ) . parse ( ) . unwrap_or ( 0 ) ;
4061+ // max_array_entries=100 means at most 100 entries created
4062+ assert ! (
4063+ count <= 100 ,
4064+ "Array entries should be capped at max_array_entries, got {}" ,
4065+ count
4066+ ) ;
4067+ }
4068+
4069+ /// TM-DOS-041: Printf format repeat via brace expansion.
4070+ /// `{1..999999999}` must be rejected by the brace expansion cap before
4071+ /// printf ever runs. Without the cap, this would generate ~1B arguments.
4072+ #[ tokio:: test]
4073+ async fn brace_expansion_bomb_printf ( ) {
4074+ let limits = ExecutionLimits :: new ( )
4075+ . max_commands ( 1_000 )
4076+ . max_stdout_bytes ( 1_000_000 ) ;
4077+ let mut bash = Bash :: builder ( ) . limits ( limits) . build ( ) ;
4078+
4079+ let result = bash. exec ( "printf '%0.s-' {1..999999999}" ) . await ;
4080+ // Either the brace expansion is capped (producing truncated output)
4081+ // or the parser rejects it statically. Either way: no OOM, no hang.
4082+ match result {
4083+ Ok ( r) => {
4084+ // If it succeeded, the output must be bounded (cap at 100K expansions or stdout limit)
4085+ assert ! (
4086+ r. stdout. len( ) <= 1_000_000 ,
4087+ "Brace expansion bomb produced {} bytes — should be capped" ,
4088+ r. stdout. len( )
4089+ ) ;
4090+ }
4091+ Err ( e) => {
4092+ // Static rejection by parser budget is also acceptable
4093+ let msg = e. to_string ( ) ;
4094+ assert ! (
4095+ msg. contains( "brace" )
4096+ || msg. contains( "range" )
4097+ || msg. contains( "too large" )
4098+ || msg. contains( "exceeded" )
4099+ || msg. contains( "budget" ) ,
4100+ "Expected brace/range limit error, got: {}" ,
4101+ msg
4102+ ) ;
4103+ }
4104+ }
4105+ }
4106+
4107+ /// TM-DOS-059: Parameter expansion replacement bomb.
4108+ /// `${x//a/$(echo bbbbbbbb)}` replaces each 'a' with 'bbbbbbbb'.
4109+ /// At scale (10K 'a's × 1K 'b's = 10MB), this must be caught by
4110+ /// max_total_variable_bytes or max_stdout_bytes.
4111+ #[ tokio:: test]
4112+ async fn parameter_expansion_replacement_bomb ( ) {
4113+ let mem = MemoryLimits :: new ( ) . max_total_variable_bytes ( 100_000 ) ;
4114+ let limits = ExecutionLimits :: new ( )
4115+ . max_commands ( 50_000 )
4116+ . max_loop_iterations ( 50_000 )
4117+ . max_total_loop_iterations ( 50_000 )
4118+ . max_stdout_bytes ( 1_000_000 ) ;
4119+ let mut bash = Bash :: builder ( )
4120+ . limits ( limits)
4121+ . memory_limits ( mem)
4122+ . session_limits ( SessionLimits :: unlimited ( ) )
4123+ . build ( ) ;
4124+
4125+ // Create a string of 10K 'a' chars, then replace each with 1K 'b' chars
4126+ // This attempts 10K × 1K = 10MB output
4127+ let script = r#"
4128+ x=$(printf 'a%.0s' {1..10000})
4129+ echo "${x//a/$(printf 'b%.0s' {1..1000})}"
4130+ "# ;
4131+ let result = bash. exec ( script) . await ;
4132+ match result {
4133+ Ok ( r) => {
4134+ // If it completes, output must be bounded by limits
4135+ assert ! (
4136+ r. stdout. len( ) <= 1_000_000 ,
4137+ "Expansion bomb produced {} bytes of stdout — should be capped" ,
4138+ r. stdout. len( )
4139+ ) ;
4140+ }
4141+ Err ( _) => {
4142+ // Limit enforcement error is also acceptable
4143+ }
4144+ }
4145+ }
4146+ }
0 commit comments