3.2.2. Структурирование кода для защиты разделяемых данных
Как мы только что видели, для защиты данных с помощью мьютекса недостаточно просто «воткнуть» объект std::lock_guard
в каждую функцию-член: один-единственный «отбившийся» указатель или ссылка сводит всю защиту на нет. На некотором уровне проверить наличие таких отбившихся указателей легко — если ни одна функция-член не передает вызывающей программе указатель или ссылку на защищенные данные в виде возвращаемого значения или выходного параметра, то данные в безопасности. Но стоит копнуть чуть глубже, как выясняется, что всё не так просто, — а просто никогда не бывает. Недостаточно проверить, что функции-члены не возвращают указатели и ссылки вызывающей программе, нужно еще убедиться, что такие указатели и ссылки не передаются в виде входных параметров вызываемым ими функциям, которые вы не контролируете. Это ничуть не менее опасно — что, если такая функция сохранит где-то указатель или ссылку, а потом какой-то другой код обратится к данным, не захватив предварительно мьютекс? Особенно следует остерегаться функций, которые передаются во время выполнения в виде аргументов или иными способами, как показано в листинге 3.2.
Листинг 3.2. Непреднамеренная передача наружу ссылки на защищённые данные
class some_data {
int а;
std::string b;
public:
void do_something();
};
class data_wrapper {
private:
some_data data;
std::mutex m;
public :
template<typename Function>
void process_data(Function func)
(1) Передаем
{ │
"защищенные"
std::lock_guard<std::mutex> l(m);│
данные поль-
func(data); ←┘
зовательской
}
функции
};
some_data* unprotected;
void malicious_function(some_data& protected_data) {
unprotected = &protected_data;
}
data_wrapper x;
void foo
(2) Передаем
{ │
вредоносную
x.process_data(malicious_function); ←┘
функцию
unprotected->do_something(); ←
(3) Доступ к "защищенным"
}
данным в обход защиты
В этом примере функция-член process_data
выглядит вполне безобидно, доступ к данным охраняется объектом std::lock_guard
, однако наличие обращения к переданной пользователем функции func
(1) означает, что foo
может передать вредоносную функцию malicious_function
, чтобы обойти защиту (2), а затем вызвать do_something()
, не захватив предварительно мьютекс (3).
Здесь фундаментальная проблема заключается в том, что мы не сделали того, что собирались сделать: пометить все участки кода, в которых имеется доступ к структуре данных, как взаимно исключающие. В данном случае мы забыли о коде внутри foo()
, который вызывает unprotected->do_something()
. К сожалению, в этом стандартная библиотека С++ нам помочь не в силах: именно программист должен позаботиться о том, чтобы защитить данные мьютексом. Но не всё так мрачно — следование приведенной ниже рекомендации выручит в таких ситуациях. Не передавайте указатели и ссылки на защищенные данные за пределы области видимости блокировки никаким способом, будь то возврат из функции, сохранение в видимой извне памяти или передача в виде аргумента пользовательской функции.
Хотя описанная только что ситуация — самая распространенная ошибка при защите разделяемых данных, перечень подводных камней ей отнюдь не исчерпывается. В следующем разделе мы увидим, что гонка возможна даже, если данные защищены мьютексом.