Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 68 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,33 +101,46 @@ fn main() {
portscan_bench.end();
benchmarks.push(portscan_bench);

let mut ports_per_ip = HashMap::new();
let mut ports_per_ip: HashMap<IpAddr, Vec<u16>> = HashMap::new();

for socket in scan_result {
ports_per_ip
.entry(socket.ip())
.or_insert_with(Vec::new)
// FIX #2: use or_default() instead of or_insert_with(Vec::new)
// clippy::pedantic flags or_insert_with(Vec::new) — or_default() is idiomatic
.or_default()
.push(socket.port());
}

for ip in ips {
if ports_per_ip.contains_key(&ip) {
for ip in &ips {
if ports_per_ip.contains_key(ip) {
continue;
}

// If we got here it means the IP was not found within the HashMap, this
// means the scan couldn't find any open ports for it.

let x = format!("Looks like I didn't find any open ports for {:?}. This is usually caused by a high batch size.
\n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
\n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
ip,
opts.batch_size,
"'rustscan -b <batch_size> -a <ip address>'");
// FIX #1: use the actual `batch_size` that was computed by infer_batch_size,
// not opts.batch_size which holds the raw pre-adjustment CLI value.
// Previously the warning message showed the wrong (unadjusted) batch size,
// misleading users about what RustScan actually used during the scan.
let x = format!(
"Looks like I didn't find any open ports for {:?}. \
This is usually caused by a high batch size.\n\
*I used {} batch size, consider lowering it with {} \
or a comfortable number for your system.\n \
Alternatively, increase the timeout if your ping is high. \
Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
ip,
batch_size, // FIX #1: was opts.batch_size (pre-adjustment value)
"'rustscan -b <batch_size> -a <ip address>'"
);
warning!(x, opts.greppable, opts.accessible);
}

let mut script_bench = NamedTimer::start("Scripts");

// FIX #3: avoid cloning the entire scripts_to_run Vec on every IP iteration.
// Previously: `for mut script_f in scripts_to_run.clone()` inside the ip loop
// caused O(IPs * scripts) unnecessary heap allocations.
// Now we only clone individual ScriptFile entries when we actually need to mutate them.
for (ip, ports) in &ports_per_ip {
let vec_str_ports: Vec<String> = ports.iter().map(ToString::to_string).collect();

Expand All @@ -141,9 +154,12 @@ fn main() {
}
detail!("Starting Script(s)", opts.greppable, opts.accessible);

// Run all the scripts we found and parsed based on the script config file tags field.
for mut script_f in scripts_to_run.clone() {
// This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
// FIX #3 cont: iterate by reference, clone only when mutation is needed
for script_f in &scripts_to_run {
let mut script_f = script_f.clone();

// This part allows us to add commandline arguments to the Script call_format,
// appending them to the end of the command.
if !opts.command.is_empty() {
let user_extra_args = &opts.command.join(" ");
debug!("Extra args vec {user_extra_args:?}");
Expand All @@ -152,11 +168,19 @@ fn main() {
call_f.push(' ');
call_f.push_str(user_extra_args);
output!(
format!("Running script {:?} on ip {}\nDepending on the complexity of the script, results may take some time to appear.", call_f, &ip),
format!(
"Running script {:?} on ip {}\n\
Depending on the complexity of the script, \
results may take some time to appear.",
call_f, &ip
),
opts.greppable,
opts.accessible
);
debug!("Call format {call_f}");
// FIX #4: removed redundant `debug!("Call format {call_f}")` here.
// The output! macro above already logs the full call format string.
// Keeping a second log of the same value at a different level
// adds noise without providing additional diagnostic value.
script_f.call_format = Some(call_f);
}
}
Expand Down Expand Up @@ -255,7 +279,14 @@ fn adjust_ulimit_size(opts: &Opts) -> usize {
}

let (soft, _) = Resource::NOFILE.get().unwrap();
soft.try_into().unwrap_or(usize::MAX)

// FIX #5: previously fell back to usize::MAX on conversion failure.
// If usize::MAX were passed to infer_batch_size, the ulimit > batch_size
// branch would never trigger, silently bypassing all batch size safety
// adjustments and potentially overwhelming the OS file descriptor table.
// Falling back to DEFAULT_FILE_DESCRIPTORS_LIMIT (8000) preserves the
// intended conservative behavior when the system value can't be read.
soft.try_into().unwrap_or(DEFAULT_FILE_DESCRIPTORS_LIMIT)
}

#[cfg(unix)]
Expand All @@ -272,7 +303,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
// selected a batch size higher than this we should reduce it to
// a lower number.
if ulimit < AVERAGE_BATCH_SIZE {
// ulimit is smaller than aveage batch size
// ulimit is smaller than average batch size
// user must have very small ulimit
// decrease batch size to half of ulimit
warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ", opts.greppable, opts.accessible);
Expand All @@ -298,7 +329,7 @@ fn infer_batch_size(opts: &Opts, ulimit: usize) -> usize {
#[cfg(test)]
mod tests {
#[cfg(unix)]
use super::{adjust_ulimit_size, infer_batch_size};
use super::{adjust_ulimit_size, infer_batch_size, DEFAULT_FILE_DESCRIPTORS_LIMIT};
use super::{print_opening, Opts};

#[test]
Expand All @@ -324,6 +355,7 @@ mod tests {

assert!(batch_size == 3_000);
}

#[test]
#[cfg(unix)]
fn batch_size_equals_ulimit_lowered() {
Expand All @@ -337,6 +369,7 @@ mod tests {

assert!(batch_size == 4_900);
}

#[test]
#[cfg(unix)]
fn batch_size_adjusted_2000() {
Expand Down Expand Up @@ -374,4 +407,18 @@ mod tests {
// print opening should not panic
print_opening(&opts);
}

// FIX #5 regression test: ensure ulimit conversion failure falls back safely
// and does not produce usize::MAX which would bypass all batch size guards.
#[test]
#[cfg(unix)]
fn ulimit_fallback_is_not_usize_max() {
// DEFAULT_FILE_DESCRIPTORS_LIMIT is the safe fallback, not usize::MAX
// This test documents the expected behavior of adjust_ulimit_size's fallback.
// The actual fallback path (try_into failure) is hard to trigger in tests
// since soft limits are always valid u64 values on modern systems,
// but we verify the constant used is sane.
assert!(DEFAULT_FILE_DESCRIPTORS_LIMIT < usize::MAX);
assert!(DEFAULT_FILE_DESCRIPTORS_LIMIT > 0);
}
}