Skip to content

Problem with borrow checker and AddAssign #52126

Closed
@sebpuetz

Description

@sebpuetz

Hi,

there seems to be a problem with the borrow checker that allows references to exist after the borrowed data has been dropped.

I wrote a wrapper struct for a HashMap that would allow me to add frequencies in a manner similar to the Counter known from Python.

When using the += operator to add two of these structs the borrow checker seems to be unable to detect when borrowed values are dropped. The code compiles allowing invalid pointers.
The compiler rejects the code when .add_assign() is called explicitly.

I tried this code (Playground):

use std::collections::HashMap;
use std::hash::Hash;
use std::ops::AddAssign;

pub fn main() {
    works();
    panics();
}

pub struct Counter<K> {
    map: HashMap<K, usize>,
}

impl<K> AddAssign for Counter<K>
where
    K: Hash + Eq,
{
    fn add_assign(&mut self, rhs: Counter<K>) {
        rhs.map.into_iter().for_each(|(key, val)| {
            let count = self.map.entry(key).or_insert(0);
            *count += val;
        });
    }
}

/// reliably prints correct
pub fn works() {
    let mut acc = Counter{map: HashMap::new()};
    for line in vec!["12345678".to_string(), "12345678".to_string()] {
        let v: Vec<&str> = line.split_whitespace().collect();
        println!("accumulator before add_assign {:?}", acc.map);
        let mut map = HashMap::new();
        for str_ref in v {
            {
                let count = map.entry(str_ref).or_insert(0);
                *count += 1;
            }
        }
        let cnt2 = Counter{map};
        acc += cnt2;
        println!("accumulator after add_assign {:?}", acc.map);
    }
}

/// often times crashes, if not prints invalid strings
pub fn panics() {
    let mut acc = Counter{map: HashMap::new()};
    for line in vec!["123456789".to_string(), "12345678".to_string()] {
        let v: Vec<&str> = line.split_whitespace().collect();
        println!("accumulator before add_assign {:?}", acc.map);
        let mut map = HashMap::new();
        for str_ref in v {
            {
                let count = map.entry(str_ref).or_insert(0);
                *count += 1;
            }
        }
        let cnt2 = Counter{map};
        acc += cnt2;
        println!("accumulator after add_assign {:?}", acc.map);
        // line gets dropped here but references are kept in acc.map
    }
}

I expected to see this happen:
The compiler should reject this code

Instead, this happened:
The program compiles and results in unpredictable behaviour. Sometimes the program panics at runtime, sometimes invalid strings are printed.

Meta

rustc --version --verbose:
rustc 1.26.0 (a775680 2018-05-07)
binary: rustc
commit-hash: a775680
commit-date: 2018-05-07
host: x86_64-unknown-linux-gnu
release: 1.26.0
LLVM version: 6.0

Backtrace:

thread 'main' panicked at 'byte index 3 is not a char boundary; it is inside 'B' (bytes 2..3) of `� B��� `', libcore/str/mod.rs:2252:5
stack backtrace:
  0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace
            at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
  1: std::sys_common::backtrace::print
            at libstd/sys_common/backtrace.rs:71
            at libstd/sys_common/backtrace.rs:59
  2: std::panicking::default_hook::{{closure}}
            at libstd/panicking.rs:207
  3: std::panicking::default_hook
            at libstd/panicking.rs:223
  4: std::panicking::rust_panic_with_hook
            at libstd/panicking.rs:402
  5: std::panicking::begin_panic_fmt
            at libstd/panicking.rs:349
  6: rust_begin_unwind
            at libstd/panicking.rs:325
  7: core::panicking::panic_fmt
            at libcore/panicking.rs:72
  8: core::str::slice_error_fail
            at libcore/str/mod.rs:0
  9: core::str::traits::<impl core::slice::SliceIndex<str> for core::ops::range::Range<usize>>::index::{{closure}}
            at libcore/str/mod.rs:1915
 10: <str as core::fmt::Debug>::fmt
            at libcore/option.rs:376
            at libcore/str/mod.rs:1915
            at libcore/str/mod.rs:1697
            at libcore/fmt/mod.rs:1818
 11: <&'a T as core::fmt::Debug>::fmt
            at /checkout/src/libcore/fmt/mod.rs:1768
 12: <&'a T as core::fmt::Debug>::fmt
            at /checkout/src/libcore/fmt/mod.rs:1768
 13: core::fmt::builders::DebugMap::entry
            at libcore/fmt/builders.rs:502
            at libcore/result.rs:621
            at libcore/fmt/builders.rs:486
 14: core::fmt::builders::DebugMap::entries
            at /checkout/src/libcore/fmt/builders.rs:520
 15: <std::collections::hash::map::HashMap<K, V, S> as core::fmt::Debug>::fmt
            at /checkout/src/libstd/collections/hash/map.rs:1487
 16: core::fmt::write
            at libcore/fmt/mod.rs:1100
            at libcore/fmt/mod.rs:1047
 17: <std::io::stdio::Stdout as std::io::Write>::write_fmt
            at libstd/io/mod.rs:1169
            at libstd/io/stdio.rs:459
 18: <std::thread::local::LocalKey<T>>::try_with
            at libstd/io/stdio.rs:686
            at libstd/thread/local.rs:290
 19: std::io::stdio::_print
            at libstd/io/stdio.rs:680
            at libstd/io/stdio.rs:701
 20: counter::panics
            at src/main.rs:51
 21: counter::main
            at src/main.rs:7
 22: std::rt::lang_start::{{closure}}
            at /checkout/src/libstd/rt.rs:74
 23: std::panicking::try::do_call
            at libstd/rt.rs:59
            at libstd/panicking.rs:306
 24: __rust_maybe_catch_panic
            at libpanic_unwind/lib.rs:102
 25: std::rt::lang_start_internal
            at libstd/panicking.rs:285
            at libstd/panic.rs:361
            at libstd/rt.rs:58
 26: std::rt::lang_start
            at /checkout/src/libstd/rt.rs:74
 27: main
 28: __libc_start_main
 29: _start

Metadata

Metadata

Assignees

Labels

A-borrow-checkerArea: The borrow checkerC-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions