Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Consolidated Security Fixes for 2.0.9 - 2.1.x #9869

Merged
merged 2 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libraries/chain/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,7 @@ struct controller_impl {
if( std::holds_alternative<packed_transaction>(receipt.trx)) {
const auto& pt = std::get<packed_transaction>(receipt.trx);
transaction_metadata_ptr trx_meta_ptr = trx_lookup ? trx_lookup( pt.id() ) : transaction_metadata_ptr{};
if( trx_meta_ptr && *trx_meta_ptr->packed_trx() != pt ) trx_meta_ptr = nullptr;
if( trx_meta_ptr && ( skip_auth_checks || !trx_meta_ptr->recovered_keys().empty() ) ) {
trx_metas.emplace_back( std::move( trx_meta_ptr ), recover_keys_future{} );
} else if( skip_auth_checks ) {
Expand Down
38 changes: 38 additions & 0 deletions libraries/chain/include/eosio/chain/transaction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -211,35 +211,67 @@ namespace eosio { namespace chain {

struct none {
digest_type digest;

digest_type prunable_digest() const;

friend bool operator==(const none& lhs, const none& rhs) {
return lhs.digest == rhs.digest;
}
friend bool operator!=(const none& lhs, const none& rhs) { return !(lhs == rhs); }
};

using segment_type = std::variant<digest_type, bytes>;

struct partial {
std::vector<signature_type> signatures;
std::vector<segment_type> context_free_segments;

digest_type prunable_digest() const;

friend bool operator==(const partial& lhs, const partial& rhs) {
return std::tie( lhs.signatures, lhs.context_free_segments ) ==
std::tie( rhs.signatures, rhs.context_free_segments );
}
friend bool operator!=(const partial& lhs, const partial& rhs) { return !(lhs == rhs); }
};

struct full {
std::vector<signature_type> signatures;
std::vector<bytes> context_free_segments;

digest_type prunable_digest() const;

friend bool operator==(const full& lhs, const full& rhs) {
return std::tie( lhs.signatures, lhs.context_free_segments ) ==
std::tie( rhs.signatures, rhs.context_free_segments );
}
friend bool operator!=(const full& lhs, const full& rhs) { return !(lhs == rhs); }
};

struct full_legacy {
std::vector<signature_type> signatures;
bytes packed_context_free_data;
vector<bytes> context_free_segments;

digest_type prunable_digest() const;

friend bool operator==(const full_legacy& lhs, const full_legacy& rhs) {
return std::tie( lhs.signatures, lhs.packed_context_free_data, lhs.context_free_segments ) ==
std::tie( rhs.signatures, rhs.packed_context_free_data, rhs.context_free_segments );
}
friend bool operator!=(const full_legacy& lhs, const full_legacy& rhs) { return !(lhs == rhs); }
};

using prunable_data_t = std::variant< full_legacy,
none,
partial,
full >;

friend bool operator==(const prunable_data_type& lhs, const prunable_data_type& rhs) {
return lhs.prunable_data == rhs.prunable_data;
}
friend bool operator!=(const prunable_data_type& lhs, const prunable_data_type& rhs) { return !(lhs == rhs); }

prunable_data_type prune_all() const;
digest_type digest() const;

Expand All @@ -265,6 +297,12 @@ namespace eosio { namespace chain {

packed_transaction_v0_ptr to_packed_transaction_v0() const;

friend bool operator==(const packed_transaction& lhs, const packed_transaction& rhs) {
return std::tie(lhs.compression, lhs.prunable_data, lhs.packed_trx) ==
std::tie(rhs.compression, rhs.prunable_data, rhs.packed_trx);
}
friend bool operator!=(const packed_transaction& lhs, const packed_transaction& rhs) { return !(lhs == rhs); }

uint32_t get_unprunable_size()const;
uint32_t get_prunable_size()const;
uint32_t get_estimated_size()const { return estimated_size; }
Expand Down
88 changes: 85 additions & 3 deletions plugins/producer_plugin/producer_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin
uint32_t _max_block_cpu_usage_threshold_us = 0;
uint32_t _max_block_net_usage_threshold_bytes = 0;
int32_t _max_scheduled_transaction_time_per_block_ms = 0;
bool _disable_persist_until_expired = false;
fc::time_point _irreversible_block_time;

std::vector<chain::digest_type> _protocol_features_to_activate;
Expand Down Expand Up @@ -581,7 +582,7 @@ class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin
send_response( e_ptr );
}
} else {
if( persist_until_expired ) {
if( persist_until_expired && !_disable_persist_until_expired ) {
// if this trx didnt fail/soft-fail and the persist flag is set, store its ID so that we can
// ensure its applied to all future speculative blocks as well.
_unapplied_transactions.add_persisted( trx );
Expand Down Expand Up @@ -716,6 +717,8 @@ void producer_plugin::set_program_options(
"ratio between incoming transactions and deferred transactions when both are queued for execution")
("incoming-transaction-queue-size-mb", bpo::value<uint16_t>()->default_value( 1024 ),
"Maximum size (in MiB) of the incoming transaction queue. Exceeding this value will subjectively drop transaction with resource exhaustion.")
("disable-api-persisted-trx", bpo::bool_switch()->default_value(false),
"Disable the re-apply of API transactions.")
("producer-threads", bpo::value<uint16_t>()->default_value(config::default_controller_thread_pool_size),
"Number of worker threads in producer thread pool")
("snapshots-dir", bpo::value<bfs::path>()->default_value("snapshots"),
Expand Down Expand Up @@ -850,6 +853,8 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_

my->_incoming_defer_ratio = options.at("incoming-defer-ratio").as<double>();

my->_disable_persist_until_expired = options.at("disable-api-persisted-trx").as<bool>();

auto thread_pool_size = options.at( "producer-threads" ).as<uint16_t>();
EOS_ASSERT( thread_pool_size > 0, plugin_config_exception,
"producer-threads ${num} must be greater than 0", ("num", thread_pool_size));
Expand Down Expand Up @@ -1713,10 +1718,79 @@ bool producer_plugin_impl::remove_expired_blacklisted_trxs( const fc::time_point
return !exhausted;
}

namespace {
// track multiple deadline / transaction cpu exceeded exceptions on unapplied transactions
class account_failures {
public:
constexpr static uint32_t max_failures_per_account = 3;

void add( const account_name& n, int64_t exception_code ) {
if( exception_code == deadline_exception::code_value || exception_code == tx_cpu_usage_exceeded::code_value ) {
auto& fa = failed_accounts[n];
++fa.num_failures;
fa.add( exception_code );
}
}

// return true if exceeds max_failures_per_account and should be dropped
bool failure_limit( const account_name& n ) {
auto fitr = failed_accounts.find( n );
if( fitr != failed_accounts.end() && fitr->second.num_failures >= max_failures_per_account ) {
++fitr->second.num_failures;
return true;
}
return false;
}

void report() const {
if( _log.is_enabled( fc::log_level::debug ) ) {
for( const auto& e : failed_accounts ) {
std::string reason;
if( e.second.num_failures > max_failures_per_account ) {
reason.clear();
if( e.second.is_deadline() ) reason += "deadline";
if( e.second.is_tx_cpu_usage() ) {
if( !reason.empty() ) reason += ", ";
reason += "tx_cpu_usage";
}
fc_dlog( _log, "Dropped ${n} trxs, account: ${a}, reason: ${r} exceeded",
("n", e.second.num_failures - max_failures_per_account)("a", e.first)("r", reason) );
}
}
}
}

private:
struct account_failure {
enum class ex_fields : uint8_t {
ex_deadline_exception = 1,
ex_tx_cpu_usage_exceeded = 2
};

void add(int64_t exception_code = 0) {
if( exception_code == tx_cpu_usage_exceeded::code_value ) {
ex_flags = set_field( ex_flags, ex_fields::ex_tx_cpu_usage_exceeded );
} else if( exception_code == deadline_exception::code_value ) {
ex_flags = set_field( ex_flags, ex_fields::ex_deadline_exception );
}
}
bool is_deadline() const { return has_field( ex_flags, ex_fields::ex_deadline_exception ); }
bool is_tx_cpu_usage() const { return has_field( ex_flags, ex_fields::ex_tx_cpu_usage_exceeded ); }

uint32_t num_failures = 0;
uint8_t ex_flags = 0;
};

std::map<account_name, account_failure> failed_accounts;
};

} // anonymous namespace

bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadline )
{
bool exhausted = false;
if( !_unapplied_transactions.empty() ) {
account_failures account_fails;
chain::controller& chain = chain_plug->chain();
int num_applied = 0, num_failed = 0, num_processed = 0;
auto unapplied_trxs_size = _unapplied_transactions.size();
Expand All @@ -1736,11 +1810,16 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin
try {
auto start = fc::time_point::now();
auto trx_deadline = start + fc::milliseconds( _max_transaction_time_ms );
if( account_fails.failure_limit( trx->packed_trx()->get_transaction().first_authorizer() ) ) {
++num_failed;
itr = _unapplied_transactions.erase( itr );
continue;
}

auto prev_billed_cpu_time_us = trx->billed_cpu_time_us;
if( prev_billed_cpu_time_us > 0 ) {
auto prev_billed_plus50 = prev_billed_cpu_time_us + EOS_PERCENT( prev_billed_cpu_time_us, 50 * config::percent_1 );
auto trx_dl = start + fc::microseconds( prev_billed_plus50 );
auto prev_billed_plus100 = prev_billed_cpu_time_us + EOS_PERCENT( prev_billed_cpu_time_us, 100 * config::percent_1 );
auto trx_dl = start + fc::microseconds( prev_billed_plus100 );
if( trx_dl < trx_deadline ) trx_deadline = trx_dl;
}
bool deadline_is_subjective = false;
Expand All @@ -1760,10 +1839,12 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin
}
// don't erase, subjective failure so try again next time
} else {
auto failure_code = trace->except->code();
// this failed our configured maximum transaction time, we don't want to replay it
fc_dlog( _log, "Failed ${c} trx, prev billed: ${p}us, ran: ${r}us, id: ${id}",
("c", trace->except->code())("p", prev_billed_cpu_time_us)
("r", fc::time_point::now() - start)("id", trx->id()) );
account_fails.add( trx->packed_trx()->get_transaction().first_authorizer(), failure_code );
++num_failed;
if( itr->next ) itr->next( trace );
itr = _unapplied_transactions.erase( itr );
Expand All @@ -1783,6 +1864,7 @@ bool producer_plugin_impl::process_unapplied_trxs( const fc::time_point& deadlin

fc_dlog( _log, "Processed ${m} of ${n} previously applied transactions, Applied ${applied}, Failed/Dropped ${failed}",
("m", num_processed)( "n", unapplied_trxs_size )("applied", num_applied)("failed", num_failed) );
account_fails.report();
}
return !exhausted;
}
Expand Down