Skip to content

Off-by-one error in parse_duration can lead to panic on user-controlled input #66

@meng-xu-cs

Description

@meng-xu-cs

Summary

parse_duration can panic on user-controlled input instead of returning Err(DurationError::NumberOverflow).

The problem is in src/duration.rs in add_current:

fn add_current(mut sec: u64, nsec: u64, out: &mut Duration) -> Result<(), Error> {
    let mut nsec = (out.subsec_nanos() as u64).add(nsec)?;
    if nsec > 1_000_000_000 {
        sec = sec.add(nsec / 1_000_000_000)?;
        nsec %= 1_000_000_000;
    }
    sec = out.as_secs().add(sec)?;
    *out = Duration::new(sec, nsec as u32);
    Ok(())
}

The guard should be >= 1_000_000_000, not > 1_000_000_000.

When the accumulated nanoseconds are exactly 1_000_000_000, the carry is skipped and the unnormalized value is passed to Duration::new. If the seconds field is already u64::MAX, Duration::new panics while trying to carry the extra second.

Reproduction

Minimal reproduction:

fn main() {
    let _ = humantime::parse_duration("18446744073709551615s 1000000000ns");
}

This can also be reproduced with accumulated nanos across multiple pieces:

fn main() {
    let _ = humantime::parse_duration("18446744073709551615s 999999999ns 1ns");
}

A regression test that shows the intended behavior:

#[test]
fn parse_duration_returns_error_instead_of_panicking_on_exact_nanos_carry_overflow() {
    let result = std::panic::catch_unwind(|| {
        humantime::parse_duration("18446744073709551615s 1000000000ns")
    });

    assert!(result.is_ok(), "parse_duration panicked");
    assert_eq!(
        result.unwrap(),
        Err(humantime::DurationError::NumberOverflow)
    );
}

Why this happens

For the input 18446744073709551615s 1000000000ns:

  1. 18446744073709551615s sets the accumulated duration to u64::MAX seconds.
  2. 1000000000ns makes the temporary nanosecond total exactly 1_000_000_000.
  3. The current condition if nsec > 1_000_000_000 does not run for that exact value.
  4. Duration::new(u64::MAX, 1_000_000_000) then tries to carry +1 second and panics.

Suggested fix

Change the normalization condition from > to >=:

if nsec >= 1_000_000_000 {
    sec = sec.add(nsec / 1_000_000_000)?;
    nsec %= 1_000_000_000;
}

That keeps the carry inside the existing checked-arithmetic path and turns this case into Err(Error::NumberOverflow) instead of a panic.

It would also be good to add regression tests for:

  • exact carry with a single token: 18446744073709551615s 1000000000ns
  • exact carry via accumulation: 18446744073709551615s 999999999ns 1ns

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions