Skip to content

Commit 8201560

Browse files
committed
Auto merge of #5988 - Eh2406:explore_the_bug, r=alexcrichton
BUG fuzzing found a bug in the resolver, we need a complete set of conflicts to do backjumping As mentioned in #5921 (comment), the new proptest found a live bug! This PR so far tracs my attempt to minimize the problematic input. The problem turned out to be that we where backjumping on incomplete set of conflicts.
2 parents b7ef8d5 + 682b295 commit 8201560

File tree

4 files changed

+216
-26
lines changed

4 files changed

+216
-26
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ crates-io = { path = "src/crates-io", version = "0.19" }
2222
crossbeam-utils = "0.5"
2323
crypto-hash = "0.3.1"
2424
curl = "0.4.13"
25-
env_logger = "0.5.4"
25+
env_logger = "0.5.11"
2626
failure = "0.1.2"
2727
filetime = "0.2"
2828
flate2 = "1.0"

src/cargo/core/resolver/conflict_cache.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::collections::{HashMap, HashSet};
22

3-
use core::{Dependency, PackageId};
4-
use core::resolver::Context;
53
use super::types::ConflictReason;
4+
use core::resolver::Context;
5+
use core::{Dependency, PackageId};
66

77
pub(super) struct ConflictCache {
88
// `con_from_dep` is a cache of the reasons for each time we
@@ -77,11 +77,17 @@ impl ConflictCache {
7777
/// `dep` is known to be unresolvable if
7878
/// all the `PackageId` entries are activated
7979
pub fn insert(&mut self, dep: &Dependency, con: &HashMap<PackageId, ConflictReason>) {
80-
let past = self.con_from_dep
80+
let past = self
81+
.con_from_dep
8182
.entry(dep.clone())
8283
.or_insert_with(Vec::new);
8384
if !past.contains(con) {
84-
trace!("{} adding a skip {:?}", dep.package_name(), con);
85+
trace!(
86+
"{} = \"{}\" adding a skip {:?}",
87+
dep.package_name(),
88+
dep.version_req(),
89+
con
90+
);
8591
past.push(con.clone());
8692
for c in con.keys() {
8793
self.dep_from_pid

src/cargo/core/resolver/mod.rs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,12 @@ fn activate_deps_loop(
305305
// It's our job here to backtrack, if possible, and find a
306306
// different candidate to activate. If we can't find any
307307
// candidates whatsoever then it's time to bail entirely.
308-
trace!("{}[{}]>{} -- no candidates", parent.name(), cur, dep.package_name());
308+
trace!(
309+
"{}[{}]>{} -- no candidates",
310+
parent.name(),
311+
cur,
312+
dep.package_name()
313+
);
309314

310315
// Use our list of `conflicting_activations` to add to our
311316
// global list of past conflicting activations, effectively
@@ -325,7 +330,12 @@ fn activate_deps_loop(
325330
past_conflicting_activations.insert(&dep, &conflicting_activations);
326331
}
327332

328-
match find_candidate(&mut backtrack_stack, &parent, &conflicting_activations) {
333+
match find_candidate(
334+
&mut backtrack_stack,
335+
&parent,
336+
backtracked,
337+
&conflicting_activations,
338+
) {
329339
Some((candidate, has_another, frame)) => {
330340
// Reset all of our local variables used with the
331341
// contents of `frame` to complete our backtrack.
@@ -432,8 +442,7 @@ fn activate_deps_loop(
432442
.clone()
433443
.filter_map(|(_, (ref new_dep, _, _))| {
434444
past_conflicting_activations.conflicting(&cx, new_dep)
435-
})
436-
.next()
445+
}).next()
437446
{
438447
// If one of our deps is known unresolvable
439448
// then we will not succeed.
@@ -467,18 +476,14 @@ fn activate_deps_loop(
467476
.iter()
468477
.flat_map(|other| other.flatten())
469478
// for deps related to us
470-
.filter(|&(_, ref other_dep)|
471-
known_related_bad_deps.contains(other_dep))
472-
.filter_map(|(other_parent, other_dep)| {
479+
.filter(|&(_, ref other_dep)| {
480+
known_related_bad_deps.contains(other_dep)
481+
}).filter_map(|(other_parent, other_dep)| {
473482
past_conflicting_activations
474-
.find_conflicting(
475-
&cx,
476-
&other_dep,
477-
|con| con.contains_key(&pid)
478-
)
479-
.map(|con| (other_parent, con))
480-
})
481-
.next()
483+
.find_conflicting(&cx, &other_dep, |con| {
484+
con.contains_key(&pid)
485+
}).map(|con| (other_parent, con))
486+
}).next()
482487
{
483488
let rel = conflict.get(&pid).unwrap().clone();
484489

@@ -518,6 +523,7 @@ fn activate_deps_loop(
518523
find_candidate(
519524
&mut backtrack_stack.clone(),
520525
&parent,
526+
backtracked,
521527
&conflicting_activations,
522528
).is_none()
523529
}
@@ -809,6 +815,7 @@ fn compatible(a: &semver::Version, b: &semver::Version) -> bool {
809815
fn find_candidate(
810816
backtrack_stack: &mut Vec<BacktrackFrame>,
811817
parent: &Summary,
818+
backtracked: bool,
812819
conflicting_activations: &HashMap<PackageId, ConflictReason>,
813820
) -> Option<(Candidate, bool, BacktrackFrame)> {
814821
while let Some(mut frame) = backtrack_stack.pop() {
@@ -830,11 +837,20 @@ fn find_candidate(
830837
// active in this back up we know that we're guaranteed to not actually
831838
// make any progress. As a result if we hit this condition we can
832839
// completely skip this backtrack frame and move on to the next.
833-
if frame
834-
.context_backup
835-
.is_conflicting(Some(parent.package_id()), conflicting_activations)
836-
{
837-
continue;
840+
if !backtracked {
841+
if frame
842+
.context_backup
843+
.is_conflicting(Some(parent.package_id()), conflicting_activations)
844+
{
845+
trace!(
846+
"{} = \"{}\" skip as not solving {}: {:?}",
847+
frame.dep.package_name(),
848+
frame.dep.version_req(),
849+
parent.package_id(),
850+
conflicting_activations
851+
);
852+
continue;
853+
}
838854
}
839855

840856
return Some((candidate, has_another, frame));

tests/testsuite/resolve.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl<'a> ToPkgId for (&'a str, String) {
104104
}
105105

106106
macro_rules! pkg {
107-
($pkgid:expr => [$($deps:expr),+]) => ({
107+
($pkgid:expr => [$($deps:expr),+ $(,)* ]) => ({
108108
let d: Vec<Dependency> = vec![$($deps.to_dep()),+];
109109
let pkgid = $pkgid.to_pkgid();
110110
let link = if pkgid.name().ends_with("-sys") {Some(pkgid.name().as_str())} else {None};
@@ -963,6 +963,174 @@ fn resolving_with_constrained_sibling_transitive_dep_effects() {
963963
);
964964
}
965965

966+
#[test]
967+
fn incomplete_information_skiping() {
968+
// When backtracking due to a failed dependency, if Cargo is
969+
// trying to be clever and skip irrelevant dependencies, care must
970+
// be taken to not miss the transitive effects of alternatives.
971+
// Fuzzing discovered that for some reason cargo was skiping based
972+
// on incomplete information in the following case:
973+
// minimized bug found in:
974+
// https://github.com/rust-lang/cargo/commit/003c29b0c71e5ea28fbe8e72c148c755c9f3f8d9
975+
let input = vec![
976+
pkg!(("a", "1.0.0")),
977+
pkg!(("a", "1.1.0")),
978+
pkg!("b" => [dep("a")]),
979+
pkg!(("c", "1.0.0")),
980+
pkg!(("c", "1.1.0")),
981+
pkg!("d" => [dep_req("c", "=1.0")]),
982+
pkg!(("e", "1.0.0")),
983+
pkg!(("e", "1.1.0") => [dep_req("c", "1.1")]),
984+
pkg!("to_yank"),
985+
pkg!(("f", "1.0.0") => [
986+
dep("to_yank"),
987+
dep("d"),
988+
]),
989+
pkg!(("f", "1.1.0") => [dep("d")]),
990+
pkg!("g" => [
991+
dep("b"),
992+
dep("e"),
993+
dep("f"),
994+
]),
995+
];
996+
let reg = registry(input.clone());
997+
998+
let res = resolve(&pkg_id("root"), vec![dep("g")], &reg).unwrap();
999+
let package_to_yank = "to_yank".to_pkgid();
1000+
// this package is not used in the resolution.
1001+
assert!(!res.contains(&package_to_yank));
1002+
// so when we yank it
1003+
let new_reg = registry(
1004+
input
1005+
.iter()
1006+
.cloned()
1007+
.filter(|x| &package_to_yank != x.package_id())
1008+
.collect(),
1009+
);
1010+
assert_eq!(input.len(), new_reg.len() + 1);
1011+
// it should still build
1012+
assert!(resolve(&pkg_id("root"), vec![dep("g")], &new_reg).is_ok());
1013+
}
1014+
1015+
#[test]
1016+
fn incomplete_information_skiping_2() {
1017+
// When backtracking due to a failed dependency, if Cargo is
1018+
// trying to be clever and skip irrelevant dependencies, care must
1019+
// be taken to not miss the transitive effects of alternatives.
1020+
// Fuzzing discovered that for some reason cargo was skiping based
1021+
// on incomplete information in the following case:
1022+
// https://github.com/rust-lang/cargo/commit/003c29b0c71e5ea28fbe8e72c148c755c9f3f8d9
1023+
let input = vec![
1024+
pkg!(("b", "3.8.10")),
1025+
pkg!(("b", "8.7.4")),
1026+
pkg!(("b", "9.4.6")),
1027+
pkg!(("c", "1.8.8")),
1028+
pkg!(("c", "10.2.5")),
1029+
pkg!(("d", "4.1.2") => [
1030+
dep_req("bad", "=6.10.9"),
1031+
]),
1032+
pkg!(("d", "5.5.6")),
1033+
pkg!(("d", "5.6.10")),
1034+
pkg!(("to_yank", "8.0.1")),
1035+
pkg!(("to_yank", "8.8.1")),
1036+
pkg!(("e", "4.7.8") => [
1037+
dep_req("d", ">=5.5.6, <=5.6.10"),
1038+
dep_req("to_yank", "=8.0.1"),
1039+
]),
1040+
pkg!(("e", "7.4.9") => [
1041+
dep_req("bad", "=4.7.5"),
1042+
]),
1043+
pkg!("f" => [
1044+
dep_req("d", ">=4.1.2, <=5.5.6"),
1045+
]),
1046+
pkg!("g" => [
1047+
dep("bad"),
1048+
]),
1049+
pkg!(("h", "3.8.3") => [
1050+
dep_req("g", "*"),
1051+
]),
1052+
pkg!(("h", "6.8.3") => [
1053+
dep("f"),
1054+
]),
1055+
pkg!(("h", "8.1.9") => [
1056+
dep_req("to_yank", "=8.8.1"),
1057+
]),
1058+
pkg!("i" => [
1059+
dep_req("b", "*"),
1060+
dep_req("c", "*"),
1061+
dep_req("e", "*"),
1062+
dep_req("h", "*"),
1063+
]),
1064+
];
1065+
let reg = registry(input.clone());
1066+
1067+
let res = resolve(&pkg_id("root"), vec![dep("i")], &reg).unwrap();
1068+
let package_to_yank = ("to_yank", "8.8.1").to_pkgid();
1069+
// this package is not used in the resolution.
1070+
assert!(!res.contains(&package_to_yank));
1071+
// so when we yank it
1072+
let new_reg = registry(
1073+
input
1074+
.iter()
1075+
.cloned()
1076+
.filter(|x| &package_to_yank != x.package_id())
1077+
.collect(),
1078+
);
1079+
assert_eq!(input.len(), new_reg.len() + 1);
1080+
// it should still build
1081+
assert!(resolve(&pkg_id("root"), vec![dep("i")], &new_reg).is_ok());
1082+
}
1083+
1084+
#[test]
1085+
fn incomplete_information_skiping_3() {
1086+
// When backtracking due to a failed dependency, if Cargo is
1087+
// trying to be clever and skip irrelevant dependencies, care must
1088+
// be taken to not miss the transitive effects of alternatives.
1089+
// Fuzzing discovered that for some reason cargo was skiping based
1090+
// on incomplete information in the following case:
1091+
// minimized bug found in:
1092+
// https://github.com/rust-lang/cargo/commit/003c29b0c71e5ea28fbe8e72c148c755c9f3f8d9
1093+
let input = vec![
1094+
pkg!{("to_yank", "3.0.3")},
1095+
pkg!{("to_yank", "3.3.0")},
1096+
pkg!{("to_yank", "3.3.1")},
1097+
pkg!{("a", "3.3.0") => [
1098+
dep_req("to_yank", "=3.0.3"),
1099+
] },
1100+
pkg!{("a", "3.3.2") => [
1101+
dep_req("to_yank", "<=3.3.0"),
1102+
] },
1103+
pkg!{("b", "0.1.3") => [
1104+
dep_req("a", "=3.3.0"),
1105+
] },
1106+
pkg!{("b", "2.0.2") => [
1107+
dep_req("to_yank", "3.3.0"),
1108+
dep_req("a", "*"),
1109+
] },
1110+
pkg!{("b", "2.3.3") => [
1111+
dep_req("to_yank", "3.3.0"),
1112+
dep_req("a", "=3.3.0"),
1113+
] },
1114+
];
1115+
let reg = registry(input.clone());
1116+
1117+
let res = resolve(&pkg_id("root"), vec![dep_req("b", "*")], &reg).unwrap();
1118+
let package_to_yank = ("to_yank", "3.0.3").to_pkgid();
1119+
// this package is not used in the resolution.
1120+
assert!(!res.contains(&package_to_yank));
1121+
// so when we yank it
1122+
let new_reg = registry(
1123+
input
1124+
.iter()
1125+
.cloned()
1126+
.filter(|x| &package_to_yank != x.package_id())
1127+
.collect(),
1128+
);
1129+
assert_eq!(input.len(), new_reg.len() + 1);
1130+
// it should still build
1131+
assert!(resolve(&pkg_id("root"), vec![dep_req("b", "*")], &new_reg).is_ok());
1132+
}
1133+
9661134
#[test]
9671135
fn resolving_but_no_exists() {
9681136
let reg = registry(vec![]);

0 commit comments

Comments
 (0)