Skip to content

Commit 3d49a0c

Browse files
committed
map_reduce: prevent mapper exception from poisoning state
In map_reduce, if the mapper throws, we can end up with poisoned state. This is because the compiler can choose to move s->result before evaluating f.get(), so s->result gets broken (if the mapped type becomes invalid after move). Fix by triggering the exception before the moving s->result, with a sequence point to force the ordering. A unit test is added (which does fail with clang before the patch, though if the arguments are evaluated in reverse order, it can pass with a different compiler).
1 parent 0dfee8b commit 3d49a0c

File tree

2 files changed

+28
-1
lines changed

2 files changed

+28
-1
lines changed

include/seastar/core/map_reduce.hh

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ map_reduce(Iterator begin, Iterator end, Mapper&& mapper, Initial initial, Reduc
191191
while (begin != end) {
192192
ret = futurize_invoke(s->mapper, *begin++).then_wrapped([s = s.get(), ret = std::move(ret)] (auto f) mutable {
193193
try {
194-
s->result = s->reduce(std::move(s->result), f.get());
194+
auto mapped_value = f.get(); // Extract exception before touching s->result
195+
s->result = s->reduce(std::move(s->result), std::move(mapped_value));
195196
return std::move(ret);
196197
} catch (...) {
197198
return std::move(ret).then_wrapped([ex = std::current_exception()] (auto f) {

tests/unit/futures_test.cc

+26
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,32 @@ SEASTAR_TEST_CASE(test_map_reduce1_lifetime) {
862862
});
863863
}
864864

865+
SEASTAR_TEST_CASE(map_reduce_with_throwing_mapper) {
866+
try {
867+
auto vec = std::vector<int>{1, 2, 3, 4, 5, 6, 7};
868+
co_await map_reduce(
869+
vec,
870+
// Mapper: identity function, but throws
871+
[] (int x) -> future<int> {
872+
if (x == 5) {
873+
throw std::runtime_error("test");
874+
}
875+
co_return x;
876+
},
877+
// Initial value (and accumulator): move-only type
878+
std::make_unique<int>(0),
879+
// Reducer: test that it does not act on a moved-from value
880+
[] (std::unique_ptr<int> acc, int x) -> std::unique_ptr<int> {
881+
BOOST_REQUIRE(bool(acc));
882+
*acc += x;
883+
return acc;
884+
}
885+
);
886+
} catch (...) {
887+
// Exception is expected and uninteresting
888+
}
889+
}
890+
865891
// This test doesn't actually test anything - it just waits for the future
866892
// returned by sleep to complete. However, a bug we had in sleep() caused
867893
// this test to fail the sanitizer in the debug build, so this is a useful

0 commit comments

Comments
 (0)