This is an automated email from the ASF dual-hosted git repository.
wwbmmm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git
The following commit(s) were added to refs/heads/master by this push:
new 98f42bcb Harden bvar sampling against use-after-free (#3487)
98f42bcb is described below
commit 98f42bcb572c2d6a64d656a342e4708462f08b9f
Author: Bright Chen <[email protected]>
AuthorDate: Tue Aug 25 16:05:43 2026 +0800
Harden bvar sampling against use-after-free (#3487)
---
src/brpc/input_messenger.cpp | 2 +-
src/bvar/detail/percentile.h | 13 +++++
src/bvar/detail/sampler.cpp | 66 +++++++++++++++++++++--
src/bvar/detail/sampler.h | 119 +++++++++++++++++++++++++++++++++++++----
src/bvar/latency_recorder.cpp | 19 +++++++
src/bvar/passive_status.h | 12 +++--
src/bvar/recorder.h | 53 ++++++++++++++++--
src/bvar/reducer.h | 28 ++++++++--
src/bvar/window.h | 56 +++++++++++++------
test/bvar_sampler_unittest.cpp | 2 -
test/bvar_window_unittest.cpp | 40 ++++++++++++++
11 files changed, 365 insertions(+), 45 deletions(-)
diff --git a/src/brpc/input_messenger.cpp b/src/brpc/input_messenger.cpp
index 81154be1..26aac063 100644
--- a/src/brpc/input_messenger.cpp
+++ b/src/brpc/input_messenger.cpp
@@ -218,7 +218,7 @@ int InputMessenger::ProcessNewMessage(
<< "Close " << *m << " due to unknown message: "
<< butil::ToPrintable(m->_read_buf);
m->SetFailed(EINVAL, "Close %s due to unknown message",
- m->description().c_str());
+ m->description().c_str());
return -1;
} else {
LOG(WARNING) << "Close " << *m << ": " << pr.error_str();
diff --git a/src/bvar/detail/percentile.h b/src/bvar/detail/percentile.h
index 430dcfee..85e681d9 100644
--- a/src/bvar/detail/percentile.h
+++ b/src/bvar/detail/percentile.h
@@ -523,10 +523,16 @@ public:
}
VoidOp inv_op() const { return VoidOp(); }
+ // Expose the shared data carrier, so that ReducerSampler holds it instead
+ // of `this`. Sampling then keeps reading valid memory even if this
+ // Percentile is destructed before the sampler is recycled.
+ shared_combiner_type share_combiner() const { return _combiner; }
+
// The sampler for windows over percentile.
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(_debug_name);
_sampler->schedule();
}
return _sampler;
@@ -543,6 +549,9 @@ public:
// This name is useful for warning negative latencies in operator<<
void set_debug_name(const butil::StringPiece& name) {
_debug_name.assign(name.data(), name.size());
+ if (nullptr != _sampler) {
+ _sampler->set_debug_name(_debug_name);
+ }
}
private:
@@ -591,6 +600,7 @@ public:
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(_debug_name);
_sampler->schedule();
}
return _sampler;
@@ -610,6 +620,9 @@ public:
// This name is useful for warning negative latencies in operator<<
void set_debug_name(const butil::StringPiece& name) {
_debug_name.assign(name.data(), name.size());
+ if (nullptr != _sampler) {
+ _sampler->set_debug_name(_debug_name);
+ }
}
private:
diff --git a/src/bvar/detail/sampler.cpp b/src/bvar/detail/sampler.cpp
index 1b89a590..65b1f5de 100644
--- a/src/bvar/detail/sampler.cpp
+++ b/src/bvar/detail/sampler.cpp
@@ -166,9 +166,16 @@ void SamplerCollector::run() {
Sampler* s = p->value();
s->_mutex.lock();
if (!s->_used) {
+ // If the sampler is still borrowed (a Window outlived the bvar
+ // it references), deleting it would leave the borrowers with a
+ // dangling pointer, so leak it on purpose. destroy() already
+ // reported the misuse.
+ const bool leaked = s->_leaked;
s->_mutex.unlock();
p->RemoveFromList();
- delete s;
+ if (!leaked) {
+ delete s;
+ }
} else {
s->take_sample();
s->_mutex.unlock();
@@ -196,12 +203,17 @@ void SamplerCollector::run() {
}
}
-Sampler::Sampler() : _used(true) {}
+Sampler::Sampler() : _used(true), _nborrow(0), _leaked(false) {}
Sampler::~Sampler() {}
DEFINE_bool(bvar_enable_sampling, true, "is enable bvar sampling");
+DEFINE_bool(bvar_abort_on_sampler_still_borrowed, false,
+ "Abort when a bvar is destructed while its sampler is still "
+ "borrowed by a Window/PerSecond, namely the Window outlives the "
+ "bvar it references");
+
void Sampler::schedule() {
// since the SamplerCollector is initialized before the program starts
// flags will not take effect if used in the SamplerCollector constructor
@@ -211,9 +223,53 @@ void Sampler::schedule() {
}
void Sampler::destroy() {
- _mutex.lock();
- _used = false;
- _mutex.unlock();
+ int nborrow = 0;
+ std::string owner;
+ {
+ BAIDU_SCOPED_LOCK(_mutex);
+ _used = false;
+ nborrow = _nborrow;
+ if (nborrow > 0) {
+ // The owning bvar is being destructed while Window/PerSecond
objects
+ // still borrow this sampler. Leak the sampler so that the
borrowers
+ // keep pointing at valid memory (they just stop getting new
samples),
+ // which turns a use-after-free into a bounded leak.
+ _leaked = true;
+ if (!_debug_name.empty()) {
+ owner.append("bvar`").append(_debug_name).append("`");
+ } else {
+ owner.append("An unnamed bvar");
+ }
+ }
+ }
+
+ if (nborrow <= 0) {
+ return;
+ }
+ if (FLAGS_bvar_abort_on_sampler_still_borrowed) {
+ LOG(FATAL) << "Abort because " << owner << " is destructed while "
+ << nborrow << " Window/PerSecond still reference its"
+ " sampler";
+ } else {
+ LOG(ERROR) << owner << " is destructed while " << nborrow
+ << " Window/PerSecond still reference its sampler. The"
+ " bvar referenced by a Window MUST be destructed"
+ " AFTER that Window, see comments of Window in"
+ " bvar/window.h. The sampler is leaked to avoid a"
+ " dangling pointer.";
+ }
+}
+
+void Sampler::add_borrower() {
+ BAIDU_SCOPED_LOCK(_mutex);
+ ++_nborrow;
+}
+
+void Sampler::remove_borrower() {
+ BAIDU_SCOPED_LOCK(_mutex);
+ CHECK_GT(_nborrow, 0) << "remove_borrower() is called more times than "
+ "add_borrower(), which is a bug of the caller";
+ --_nborrow;
}
} // namespace detail
diff --git a/src/bvar/detail/sampler.h b/src/bvar/detail/sampler.h
index c3d60828..06533e4e 100644
--- a/src/bvar/detail/sampler.h
+++ b/src/bvar/detail/sampler.h
@@ -21,6 +21,9 @@
#define BVAR_DETAIL_SAMPLER_H
#include <vector>
+#include <string> // std::string
+#include <type_traits> // std::true_type
+#include <utility> // std::declval
#include "butil/containers/linked_list.h"// LinkNode
#include "butil/scoped_lock.h" // BAIDU_SCOPED_LOCK
#include "butil/logging.h" // LOG()
@@ -57,14 +60,41 @@ public:
// Call this function instead of delete to destroy the sampler. Deletion
// of the sampler may be delayed for seconds.
void destroy();
-
+
+ // Declare/undeclare that an external object borrows this sampler which is
+ // owned by another bvar. Window/PerSecond does this because it samples
+ // through the sampler of the bvar it references.
+ // If the owner is destructed while borrowers remain (namely a Window
+ // outlives the bvar it references, which violates the contract documented
+ // in bvar/window.h), destroy() reports the misuse and the sampler is
+ // deliberately leaked so that borrowers are not left with a dangling
+ // pointer.
+ void add_borrower();
+ void remove_borrower();
+
+ // Name of the owning bvar, purely for diagnostics.
+ void set_debug_name(const std::string& name) {
+ BAIDU_SCOPED_LOCK(_mutex);
+ _debug_name = name;
+ }
+ std::string debug_name() const {
+ BAIDU_SCOPED_LOCK(_mutex);
+ return _debug_name;
+ }
+
protected:
virtual ~Sampler();
friend class SamplerCollector;
bool _used;
- // Sync destroy() and take_sample().
- butil::Mutex _mutex;
+ // Number of external borrowers, guarded by _mutex.
+ int _nborrow;
+ // Set by destroy() when _nborrow > 0, telling the sampling thread to leak
+ // this sampler instead of deleting it. Guarded by _mutex.
+ bool _leaked;
+ mutable butil::Mutex _mutex;
+ // For diagnostics only, see set_debug_name().
+ std::string _debug_name;
};
// Representing a non-existing operator so that we can test
@@ -78,19 +108,90 @@ struct VoidOp {
}
};
+// Detects whether the host R exposes share_combiner(), namely whether its
+// sampling data lives in a shared_ptr-managed carrier (an AgentCombiner) that
+// the sampler is able to hold on its own. Hosts keeping the data elsewhere --
+// a user callback in PassiveStatus, or a value-type babylon counter -- do NOT
+// provide it and are sampled through the host pointer as before.
+template <typename R>
+class HasShareCombiner {
+ template <typename U>
+ static auto probe(U* p) -> decltype(p->share_combiner(), std::true_type());
+ static std::false_type probe(...);
+public:
+ static const bool value = decltype(probe(std::declval<R*>()))::value;
+};
+
+// Samples through the host pointer, for hosts keeping their data outside a
+// shared carrier (a user callback in PassiveStatus, a value-type babylon
+// counter, ...).
+template <typename R, typename T, typename Op, typename InvOp>
+class HostSampleSource {
+public:
+ explicit HostSampleSource(R* host)
+ : _host(host), _op(host->op()), _inv_op(host->inv_op()) {}
+
+ // Only reached from take_sample(), namely from the sampling thread, which
is
+ // mutually exclusive with the host's destroy(). The host is therefore
always
+ // alive here.
+ T reset() { return _host->reset(); }
+ T get_value() const { return _host->get_value(); }
+
+ // Never touch the host, see the ctor.
+ const Op& op() const { return _op; }
+ const InvOp& inv_op() const { return _inv_op; }
+
+private:
+ R* _host;
+ Op _op;
+ InvOp _inv_op;
+};
+
+// Samples directly from the shared data carrier, so that sampling still reads
+// valid memory even if the host is destructed before the sampler is recycled.
+// `Op'/`InvOp' are stateless functors, thus copied by value at construction
and
+// the host is never touched afterwards.
+template <typename R, typename T, typename Op, typename InvOp>
+class CombinerSampleSource {
+public:
+ explicit CombinerSampleSource(R* host)
+ : _combiner(host->share_combiner())
+ , _op(host->op())
+ , _inv_op(host->inv_op()) {}
+
+ T reset() { return _combiner->reset_all_agents(); }
+ T get_value() const { return _combiner->combine_agents(); }
+ const Op& op() const { return _op; }
+ const InvOp& inv_op() const { return _inv_op; }
+
+private:
+ typename R::shared_combiner_type _combiner;
+ Op _op;
+ InvOp _inv_op;
+};
+
// The sampler for reducer-alike variables.
// The R should have following methods:
// - T reset();
// - T get_value();
// - Op op();
// - InvOp inv_op();
+// Additionally, if R exposes
+// - shared_combiner_type share_combiner();
+// the sampler holds that shared carrier instead of R itself, which makes
+// sampling immune to R being destructed first.
template <typename R, typename T, typename Op, typename InvOp>
class ReducerSampler : public Sampler {
+ typedef typename butil::conditional<
+ HasShareCombiner<R>::value,
+ CombinerSampleSource<R, T, Op, InvOp>,
+ HostSampleSource<R, T, Op, InvOp> >::type source_type;
+
public:
static const time_t MAX_SECONDS_LIMIT = 3600;
explicit ReducerSampler(R* reducer)
- : _reducer(reducer)
+ : _source(reducer)
, _window_size(1) {
// Invoked take_sample at begining so the value of the first second
@@ -127,14 +228,14 @@ public:
// Suming up samples gives the result within a window.
// In this case, get_value() of _reducer gives wrong answer and
// should not be called.
- latest.data = _reducer->reset();
+ latest.data = _source.reset();
} else {
// The operator can be inversed.
// We save the result as a sample.
// Inversed operation between latest and oldest sample within a
// window gives result.
// get_value() of _reducer can still be called.
- latest.data = _reducer->get_value();
+ latest.data = _source.get_value();
}
latest.time_us = butil::cpuwide_time_us();
_q.elim_push(latest);
@@ -164,12 +265,12 @@ public:
if (e == oldest) {
break;
}
- _reducer->op()(result->data, e->data);
+ _source.op()(result->data, e->data);
}
} else {
// Diff the latest and oldest sample within the window.
result->data = latest->data;
- _reducer->inv_op()(result->data, oldest->data);
+ _source.inv_op()(result->data, oldest->data);
}
result->time_us = latest->time_us - oldest->time_us;
return true;
@@ -212,7 +313,7 @@ public:
}
private:
- R* _reducer;
+ source_type _source;
time_t _window_size;
butil::BoundedQueue<Sample<T> > _q;
};
diff --git a/src/bvar/latency_recorder.cpp b/src/bvar/latency_recorder.cpp
index 0bb4d5d8..5f1bd597 100644
--- a/src/bvar/latency_recorder.cpp
+++ b/src/bvar/latency_recorder.cpp
@@ -24,6 +24,25 @@
namespace bvar {
+#if !WITH_BABYLON_COUNTER
+// Verify how ReducerSampler picks its data source, using the very hosts that
+// LatencyRecorder is made of.
+// Hosts keeping their data in a shared combiner are sampled through that
carrier,
+// so sampling reads valid memory even if the host is destructed before the
sampler
+// is recycled.
+// PassiveStatus keeps its data in a user callback instead, hence it is still
sampled
+// through the host pointer.
+static_assert(detail::HasShareCombiner<IntRecorder>::value,
+ "IntRecorder should be sampled through its shared combiner");
+static_assert(detail::HasShareCombiner<Maxer<int64_t>::Base>::value,
+ "Reducer should be sampled through its shared combiner");
+static_assert(detail::HasShareCombiner<detail::Percentile>::value,
+ "Percentile should be sampled through its shared combiner");
+static_assert(!detail::HasShareCombiner<PassiveStatus<int64_t> >::value,
+ "PassiveStatus has no shared carrier, it must keep being sampled"
+ " through the host pointer");
+#endif // !WITH_BABYLON_COUNTER
+
static bool valid_percentile(const char*, int32_t v) {
return v > 0 && v < 100;
}
diff --git a/src/bvar/passive_status.h b/src/bvar/passive_status.h
index aeecec0c..7122e741 100644
--- a/src/bvar/passive_status.h
+++ b/src/bvar/passive_status.h
@@ -143,6 +143,7 @@ public:
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(name());
_sampler->schedule();
}
return _sampler;
@@ -168,11 +169,16 @@ public:
protected:
int expose_impl(const butil::StringPiece& prefix,
- const butil::StringPiece& name,
+ const butil::StringPiece& n,
DisplayFilter display_filter) override {
- const int rc = Variable::expose_impl(prefix, name, display_filter);
+ const int rc = Variable::expose_impl(prefix, n, display_filter);
+ if (rc != 0) {
+ return rc;
+ }
+ if (_sampler != nullptr) {
+ _sampler->set_debug_name(name());
+ }
if (ADDITIVE &&
- rc == 0 &&
_series_sampler == nullptr &&
FLAGS_save_series) {
_series_sampler = new SeriesSampler(this);
diff --git a/src/bvar/recorder.h b/src/bvar/recorder.h
index d535ab67..147fcb72 100644
--- a/src/bvar/recorder.h
+++ b/src/bvar/recorder.h
@@ -164,6 +164,11 @@ public:
detail::AddStat op() const { return detail::AddStat(); }
detail::MinusStat inv_op() const { return detail::MinusStat(); }
+
+ // Expose the shared data carrier, so that ReducerSampler holds it instead
+ // of `this'. Sampling then keeps reading valid memory even if this
+ // IntRecorder is destructed before the sampler is recycled.
+ shared_combiner_type share_combiner() const { return _combiner; }
void describe(std::ostream& os, bool /*quote_string*/) const override {
os << get_value();
@@ -172,8 +177,9 @@ public:
bool valid() const { return _combiner->valid(); }
sampler_type* get_sampler() {
- if (nullptr == _sampler) {
+ if (_sampler == nullptr) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(diagnostic_name());
_sampler->schedule();
}
return _sampler;
@@ -183,9 +189,27 @@ public:
// IntRecorder is often used as the source of data and not exposed.
void set_debug_name(const butil::StringPiece& name) {
_debug_name.assign(name.data(), name.size());
+ if (_sampler != nullptr) {
+ _sampler->set_debug_name(diagnostic_name());
+ }
}
-
+
+protected:
+ int expose_impl(const butil::StringPiece& prefix,
+ const butil::StringPiece& name,
+ DisplayFilter display_filter) override {
+ const int rc = Variable::expose_impl(prefix, name, display_filter);
+ if (rc == 0 && _sampler != nullptr) {
+ _sampler->set_debug_name(diagnostic_name());
+ }
+ return rc;
+ }
+
private:
+ const std::string& diagnostic_name() const {
+ return name().empty() ? _debug_name : name();
+ }
+
// TODO: The following numeric functions should be independent utils
static uint64_t _get_sum(const uint64_t n) {
return (n & MAX_SUM_PER_THREAD);
@@ -240,8 +264,8 @@ private:
private:
shared_combiner_type _combiner;
- sampler_type* _sampler;
- std::string _debug_name;
+ sampler_type* _sampler;
+ std::string _debug_name;
};
inline IntRecorder& IntRecorder::operator<<(int64_t sample) {
@@ -372,6 +396,7 @@ public:
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(diagnostic_name());
_sampler->schedule();
}
return _sampler;
@@ -381,8 +406,28 @@ public:
// IntRecorder is often used as the source of data and not exposed.
void set_debug_name(const butil::StringPiece& name) {
_debug_name.assign(name.data(), name.size());
+ if (nullptr != _sampler) {
+ _sampler->set_debug_name(diagnostic_name());
+ }
}
+
+protected:
+ int expose_impl(const butil::StringPiece& prefix,
+ const butil::StringPiece& name,
+ DisplayFilter display_filter) override {
+ const int rc = Variable::expose_impl(prefix, name, display_filter);
+ if (rc == 0 && nullptr != _sampler) {
+ _sampler->set_debug_name(diagnostic_name());
+ }
+ return rc;
+ }
+
private:
+ // See the non-babylon IntRecorder for details.
+ const std::string& diagnostic_name() const {
+ return name().empty() ? _debug_name : name();
+ }
+
babylon::ConcurrentSummer _summer;
sampler_type* _sampler{nullptr};
std::string _debug_name;
diff --git a/src/bvar/reducer.h b/src/bvar/reducer.h
index 943a2e75..9fdb1960 100644
--- a/src/bvar/reducer.h
+++ b/src/bvar/reducer.h
@@ -86,6 +86,7 @@ public:
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(name());
_sampler->schedule();
}
return _sampler;
@@ -136,7 +137,13 @@ protected:
const butil::StringPiece& name,
DisplayFilter display_filter) override {
const int rc = Variable::expose_impl(prefix, name, display_filter);
- if (rc == 0 && nullptr == _series_sampler &&
+ if (rc != 0) {
+ return rc;
+ }
+ if (nullptr != _sampler) {
+ _sampler->set_debug_name(this->name());
+ }
+ if (nullptr == _series_sampler &&
!butil::is_same<InvOp, VoidOp>::value &&
!butil::is_same<T, std::string>::value &&
FLAGS_save_series) {
@@ -255,10 +262,16 @@ public:
// Get instance of Op.
const Op& op() const { return _combiner->op(); }
const InvOp& inv_op() const { return _inv_op; }
+
+ // Expose the shared data carrier, so that ReducerSampler holds it instead
+ // of `this'. Sampling then keeps reading valid memory even if this Reducer
+ // is destructed before the sampler is recycled by the sampling thread.
+ shared_combiner_type share_combiner() const { return _combiner; }
sampler_type* get_sampler() {
if (nullptr == _sampler) {
_sampler = new sampler_type(this);
+ _sampler->set_debug_name(name());
_sampler->schedule();
}
return _sampler;
@@ -276,11 +289,16 @@ public:
protected:
int expose_impl(const butil::StringPiece& prefix,
- const butil::StringPiece& name,
+ const butil::StringPiece& n,
DisplayFilter display_filter) override {
- const int rc = Variable::expose_impl(prefix, name, display_filter);
- if (rc == 0 &&
- _series_sampler == nullptr &&
+ const int rc = Variable::expose_impl(prefix, n, display_filter);
+ if (rc != 0) {
+ return rc;
+ }
+ if (_sampler != nullptr) {
+ _sampler->set_debug_name(name());
+ }
+ if (_series_sampler == nullptr &&
!butil::is_same<InvOp, detail::VoidOp>::value &&
!butil::is_same<T, std::string>::value &&
FLAGS_save_series) {
diff --git a/src/bvar/window.h b/src/bvar/window.h
index fcd29e11..31c73b98 100644
--- a/src/bvar/window.h
+++ b/src/bvar/window.h
@@ -22,6 +22,8 @@
#include <limits> // std::numeric_limits
#include <math.h> // round
+#include <type_traits> // std::decay
+#include <utility> // std::declval
#include <gflags/gflags_declare.h>
#include "butil/logging.h" // LOG
#include "bvar/detail/sampler.h"
@@ -45,19 +47,30 @@ public:
typedef typename R::value_type value_type;
typedef typename R::sampler_type sampler_type;
- class SeriesSampler : public detail::Sampler {
+ // Type of the underlying var's operator, copied by value so that appending
+ // to the series never dereferences the var.
+ typedef typename std::decay<decltype(std::declval<R&>().op())>::type
var_op_type;
+
+ class SeriesSampler : public Sampler {
public:
+ // Holds a COPY of the underlying var's operator rather than a pointer
to
+ // the var. The operators of bvar (AddTo/MaxTo/AddStat/...) are
stateless
+ // functors, so copying is cheap and, more importantly, the sampling
+ // thread never touches the var -- which may already be destructed if
the
+ // user let this Window outlive it.
struct Op {
- explicit Op(R* var) : _var(var) {}
+ explicit Op(const var_op_type& op) : _op(op) {}
void operator()(value_type& v1, const value_type& v2) const {
- _var->op()(v1, v2);
+ _op(v1, v2);
}
private:
- R* _var;
+ var_op_type _op;
};
- SeriesSampler(WindowBase* owner, R* var)
- : _owner(owner), _series(Op(var)) {}
- ~SeriesSampler() {}
+
+ SeriesSampler(WindowBase* owner, const var_op_type& op)
+ : _owner(owner), _series(Op(op)) {}
+ ~SeriesSampler() override = default;
+
void take_sample() override {
if (series_freq == SERIES_IN_SECOND) {
// Get one-second window value for PerSecond<>, otherwise the
@@ -73,35 +86,43 @@ public:
void describe(std::ostream& os) { _series.describe(os, nullptr); }
private:
WindowBase* _owner;
- detail::Series<value_type, Op> _series;
+ Series<value_type, Op> _series;
};
WindowBase(R* var, time_t window_size)
: _var(var)
+ , _var_op(var->op())
, _window_size(window_size > 0 ? window_size :
FLAGS_bvar_dump_interval)
, _sampler(var->get_sampler())
, _series_sampler(nullptr) {
+ // Tell the borrowed sampler about us, so that destructing `var` before
+ // this Window is detected and reported instead of silently leaving
+ // `_sampler' dangling. See Sampler::add_borrower().
+ _sampler->add_borrower();
CHECK_EQ(0, _sampler->set_window_size(_window_size));
}
- ~WindowBase() {
+ ~WindowBase() override {
hide();
if (_series_sampler) {
_series_sampler->destroy();
_series_sampler = nullptr;
}
+ // Safe even if `var` was destructed first: in that case destroy()
marked
+ // the sampler as leaked, so it was not deleted by the sampling thread.
+ _sampler->remove_borrower();
}
- bool get_span(time_t window_size, detail::Sample<value_type>* result)
const {
+ bool get_span(time_t window_size, Sample<value_type>* result) const {
return _sampler->get_value(window_size, result);
}
- bool get_span(detail::Sample<value_type>* result) const {
+ bool get_span(Sample<value_type>* result) const {
return get_span(_window_size, result);
}
virtual value_type get_value(time_t window_size) const {
- detail::Sample<value_type> tmp;
+ Sample<value_type> tmp;
if (get_span(window_size, &tmp)) {
return tmp.data;
}
@@ -148,13 +169,17 @@ protected:
if (rc == 0 &&
_series_sampler == nullptr &&
FLAGS_save_series) {
- _series_sampler = new SeriesSampler(this, _var);
+ _series_sampler = new SeriesSampler(this, _var_op);
_series_sampler->schedule();
}
return rc;
}
+ // NOTE: `_var` is only dereferenced in the ctor (get_sampler()/op()). Do
NOT
+ // dereference it afterwards: the user may have destructed it already if
this
+ // Window outlives it (see the contract in comments of Window below).
R* _var;
+ var_op_type _var_op;
time_t _window_size;
sampler_type* _sampler;
SeriesSampler* _series_sampler;
@@ -289,8 +314,7 @@ public:
return *this;
}
- // Implement Variable::describe()
- void describe(std::ostream& os, bool quote_string) const {
+ void describe(std::ostream& os, bool quote_string) const override {
if (butil::is_same<value_type, std::string>::value && quote_string) {
os << '"' << get_value() << '"';
} else {
@@ -298,7 +322,7 @@ public:
}
}
- virtual ~WindowExAdapter() {
+ ~WindowExAdapter() override {
hide();
}
diff --git a/test/bvar_sampler_unittest.cpp b/test/bvar_sampler_unittest.cpp
index 40cc22d9..bfa79eed 100644
--- a/test/bvar_sampler_unittest.cpp
+++ b/test/bvar_sampler_unittest.cpp
@@ -15,9 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-#include <limits> //std::numeric_limits
#include "bvar/detail/sampler.h"
-#include "butil/time.h"
#include "butil/logging.h"
#include <gtest/gtest.h>
diff --git a/test/bvar_window_unittest.cpp b/test/bvar_window_unittest.cpp
index e50cf109..ace39bd3 100644
--- a/test/bvar_window_unittest.cpp
+++ b/test/bvar_window_unittest.cpp
@@ -23,6 +23,8 @@
#include <sstream>
#include <butil/time.h>
#include <butil/macros.h>
+#include <butil/logging.h> // logging::StringSink
+#include <butil/compiler_specific.h> // BUTIL_USE_ASAN
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include "bvar/bvar.h"
@@ -95,3 +97,41 @@ TEST_F(WindowTest, window) {
ASSERT_EQ(recorder_stat.get_average_int(),
window_ex_recorder_stat.get_average_int());
ASSERT_DOUBLE_EQ(recorder_stat.get_average_double(),
window_ex_recorder_stat.get_average_double());
}
+
+// A Window/PerSecond outliving the bvar it references violates the contract
+// documented in bvar/window.h. Before this was handled, the Window was left
with
+// a dangling sampler pointer (the sampling thread had already deleted it),
which
+// silently became a use-after-free. Now:
+// - Sampler::destroy() notices it is still borrowed, reports the misuse and
+// marks the sampler so that the sampling thread leaks it instead of
deleting
+// it, keeping the borrower's pointer valid (edge A);
+// - the series sampler holds a COPY of the var's operator, so appending to
the
+// series never dereferences the destructed var (edge B).
+//
+// NOTE: this test leaks the sampler ON PURPOSE, hence it is skipped under ASan
+// (which bundles LeakSanitizer) that would (correctly) report that leak.
+#ifndef BUTIL_USE_ASAN
+TEST_F(WindowTest, window_outliving_referenced_var) {
+ bvar::PerSecond<bvar::Adder<int64_t> >* ps = nullptr;
+ {
+ // Named so that the diagnostic below can identify the offending bvar.
+ bvar::Adder<int64_t> a("window_outliving_referenced_var_adder");
+ a << 10;
+ ps = new bvar::PerSecond<bvar::Adder<int64_t> >(&a, 1);
+ // Expose it so that a series sampler is created as well, covering the
+ // path where the series operator would touch the var (edge B).
+ ASSERT_EQ(0, ps->expose("window_outliving_referenced_var"));
+ sleep(1);
+ }
+ // `a' is destructed while `ps' still borrows its sampler. Give the
sampling
+ // thread a chance to walk the destroy branch.
+ sleep(2);
+ // Used to be a use-after-free; the sampler memory is still valid now, `ps'
+ // merely stops receiving new samples.
+ (void)ps->get_value();
+ std::ostringstream os;
+ ps->describe(os, false);
+ // remove_borrower() on the leaked sampler is safe too.
+ delete ps;
+}
+#endif // BUTIL_USE_ASAN
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]