Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Владимир Кошелев «На что способны современные статические анализаторы для C#»

Владимир Кошелев «На что способны современные статические анализаторы для C#»

При разработке на C# все мы используем статический анализ. Стандартные предупреждения Visual Studio, FxCop, Visual Studio Code Analysis, ReSharper и не только. Однако, многие относятся к статическому анализу как к черному ящику, выдающему непредсказуемые и нередко ложные результаты. Мы поговорим о типичных ситуациях, которые обнаруживают статические анализаторы, и о принципиальных проблемах, приводящих к ложным срабатываниям. Обсудим основные идеи, позволяющие реализовать анализ, работающий за разумное время. В заключение мы расскажем об анализаторе, который разрабатывается в ИСП РАН.

DotNetRu

April 02, 2016
Tweet

More Decks by DotNetRu

Other Decks in Programming

Transcript

  1. О Нас • Чем занимается ИСП РАН? • Компиляторное технологии:

    анализ и оптимизация • R&D проекты для таких компаний, как Samsung, Intel Валерий Игнатьев: к.ф.-м.н., 6 лет занимается статическим анализом. Владимир Кошелев: 4 года занимаюсь статическим анализом, пишу диссертацию про анализ C#. Артем Борзилов: 2 года занимался динамическим анализом и деобфускацией, последние 2 года - статический анализ. 02.04.2016 ИСП РАН 2
  2. Автоматический анализ программ • Задача: автоматический поиск ошибок в программах.

    • Поиск потенциальных ошибок (дефектов) в исходном коде программ • Поиск входных данных, на которых программа упадет (поиск уязвимостей) 02.04.2016 ИСП РАН 3
  3. Поиск уязвимостей: Фаззинг 02.04.2016 ИСП РАН 4 Генерация входных данных

    Средства запуска программы Программа Монитор нештатных ситуаций База «плохих» входных данных
  4. Разница между уязвимостью и дефектом • Поиск уязвимостей - это

    прежде всего поиск «плохих» входных данных. • Поиск дефектов - это поиск подозрительных мест в коде, входные данные искать не требуется. Поэтому его можно сделать менее точным и куда более быстрым, чем поиск уязвимостей. 02.04.2016 ИСП РАН 5
  5. Как оценивать качество анализа дефектов? • Фиксируем время, отведенное для

    анализа, например, 10 минут для всего проекта. • Фиксируем порог для ложных срабатываний, например, не более 50%. • Тогда анализатор должен найти как можно больше срабатываний, соблюдая предыдущие два требования. 02.04.2016 ИСП РАН 7
  6. Методы поиска дефектов • Анализ текста исходной программы • Анализ

    внутреннего представления компилятора (например, абстрактных синтаксических деревьев) • Анализ потенциальных путей выполнения программы 02.04.2016 ИСП РАН 8
  7. Поиск использований obsolete методов class Foo { [Obsolete] public void

    Request(string str) { // Some Code } } ... Foo foo; ... foo.Request("abacaba") 02.04.2016 ИСП РАН 9
  8. Ищем .Request( class Foo { [Obsolete] public void Request(string str)

    { // Some Code } } ... Foo foo; ... foo.Request("abacaba") 02.04.2016 ИСП РАН 10
  9. Проблема: поиск найдет использование всех методов Request. class Bar {

    public Bar Request(string request, string out response) ... } class Baz { public void Request(Bar request) ... } ... Bar bar; Baz baz; baz.Request(bar.Request(s1, s2)) 02.04.2016 ИСП РАН 11
  10. Можно, конечно, попытаться отфильтровать с помощью RegEx Однако, RegEx не

    может распарсить даже HTML, куда там до C#. 02.04.2016 ИСП РАН 12
  11. Абстрактное синтаксическое дерево static void Bar(bool f, Foo foo, string

    s) { if (f) { foo.Request(s); } } 02.04.2016 ИСП РАН 13 Bar bool f Foo foo string s if f (bool) call foo (Foo) Request s (string) Метод Переменная Оператор Выражение
  12. Как найти все obsolete? • Подписываемся на тип вершины call

    (для обхода АСТ используется паттерн Visitor). • Проверяем, что тип переменной - Foo. • Проверяем, что вызываемый метод – Request. 02.04.2016 ИСП РАН 14 call foo (Foo) Request s (string) call foo (Foo) Request c (char []) i (int)
  13. Поиск бесконечных циклов Правило: Если индексная переменная имеет тип int

    и увеличивается в цикле, то в условии она должна проверяться на “меньше”. for (int i = startIdx; i < endIdx; i++) { expected[i] = false; } 02.04.2016 ИСП РАН 15
  14. Пример ошибки for (int idx = startIdx; idx > endIdx;

    idx++) { expected[i] = false; } 02.04.2016 ИСП РАН 16 for = > ++ int idx startIdx idx endIdx idx
  15. Можно ли найти с помощью AST обращение к null? 02.04.2016

    ИСП РАН 17 В простом случае – можно: public static void AddRange<T>(this IList<T> list, IEnumerable<T> values) { var lt = list as List<T>; if (lt != null) lt.AddRange(values); else { foreach (var item in values) { lt.Add(item); } } }
  16. А в реальности – есть проблемы int numDiagnostics = diagnostics

    == null ? 0 : diagnostics.Count; if (numDiagnostics > 0) { foreach (var diagEntry in diagnostics) { ... } } 02.04.2016 ИСП РАН 18
  17. А в реальности – есть проблемы int numDiagnostics = diagnostics

    == null ? 0 : diagnostics.Count; if (numDiagnostics > 0) { foreach (var diagEntry in diagnostics) { ... } } Условие ошибки: 1) Если diagnostics == null, то numDiagnostics == 0 2) В foreach заходим, только если numDiagnostics > 0 Ошибка невозможна 02.04.2016 ИСП РАН 19
  18. Нужны методы анализа путей выполнения И не только для поиска

    использований null! Также можно искать: • утечку ресурсов, • использование освобожденных объектов (после Dispose), • деление на ноль, • ошибочное явное приведение типов, • дефекты, делающие возможными SQL Injection, XML Injection и т.д. 02.04.2016 ИСП РАН 20
  19. Идея символьного выполнения • Разрешим параметрам функции иметь неизвестные (символьные)

    значения: 1) int Sum(int a, int b) 2) { // a:= xa , b:= xb 3) int c = a + b; 4) int d = c*5; 5) return d; 6) } 02.04.2016 ИСП РАН 21
  20. Идея символьного выполнения • Разрешим параметрам функции иметь неизвестные (символьные)

    значения: 1) int Sum(int a, int b) 2) { // a:= xa , b:= xb 3) int c = a + b; // a:= xa , b:= xb , c:= xa +xb 4) int d = c*5; 5) return d; 6) } 02.04.2016 ИСП РАН 22
  21. Идея символьного выполнения • Разрешим параметрам функции иметь неизвестные (символьные)

    значения: 1) int Sum(int a, int b) 2) { // a:= xa , b:= xb 3) int c = a + b; // a:= xa , b:= xb , c:= xa +xb 4) int d = c*5; // a:= xa , b:= xb , c:= xa +xb , d:= (xa +xb )*5 5) return d; 6) } 02.04.2016 ИСП РАН 23
  22. Идея символьного выполнения • Разрешим параметрам функции иметь неизвестные (символьные)

    значения: 1) int Sum(int a, int b) 2) { // a:= xa , b:= xb 3) int c = a + b; // a:= xa , b:= xb , c:= xa +xb 4) int d = c*5; // a:= xa , b:= xb , c:= xa +xb , d:= (xa +xb )*5 5) return d; // a:= xa , b:= xb , c:= xa +xb , d:= (xa +xb )*5 6) } 02.04.2016 ИСП РАН 24
  23. Проблема: как обрабатывать if? 1) int Choose(int a) 2) {

    // a:= xa , 3) int c = 0; 4) if (a % 4 != 0) 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 25
  24. Проблема: как обрабатывать if? 1) int Choose(int a) 2) {

    // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 26
  25. Проблема: как обрабатывать if? 1) int Choose(int a) 2) {

    // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0, куда идти?! 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 27
  26. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; 4) if (a % 4 != 0) 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 28
  27. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 29
  28. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 30
  29. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0 5) c = 1; 6) else 7) c = 2; // a:= xa , c:= 2 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 31
  30. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0 5) c = 1; 6) else 7) c = 2; // a:= xa , c:= 2 8) if (a % 2 != 0 && c == 2) 9) c = 3; // a:= xa , c:= 3 10) return c; 11) } 02.04.2016 ИСП РАН 32
  31. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0 5) c = 1; 6) else 7) c = 2; // a:= xa , c:= 2 8) if (a % 2 != 0 && c == 2) 9) c = 3; // a:= xa , c:= 3, не могли прийти! 10) return c; 11) } 02.04.2016 ИСП РАН 33
  32. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0, [xa % 4 == 0] 5) c = 1; 6) else 7) c = 2; 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 34
  33. Давайте определимся с порядком переходов 1) int Choose(int a) 2)

    { // a:= xa , 3) int c = 0; // a:= xa , c:= 0 4) if (a % 4 != 0) // a:= xa , c:= 0, [xa % 4 == 0] 5) c = 1; 6) else 7) c = 2; // a:= xa , c:= 2, [xa % 4 == 0] 8) if (a % 2 != 0 && c == 2) 9) c = 3; 10) return c; 11) } 02.04.2016 ИСП РАН 35
  34. Символьное состояние состоит из: Значений переменных, выраженных через символьные значения

    a:= xa , c:= 2, [xa % 4 != 0], (7) Условий пройденных переходов (Предикат пути) a:= xa , c:= 2, [xa % 4 != 0], (7) Текущей точки в программе a:= xa , c:= 2, [xa % 4 != 0], (7) 02.04.2016 ИСП РАН 36
  35. Как с помощью этого искать использование null? 1) int Index(IList<string>

    diags, string elem) 2) { 3) int nd; // diags:= xd 4) if (diags == null) 5) nd = 0; 6) else 7) nd = diags.Count; 8) if (nd >= 0) 9) { 10) return diags.IndexOf(elem); 11) } 12) return -1; 13) } 02.04.2016 ИСП РАН 37
  36. Как с помощью этого искать использование null? 1) int Index(IList<string>

    diags, string elem) 2) { 3) int nd; // diags:= xd 4) if (diags == null) // diags:= xd , [xd = null] 5) nd = 0; 6) else 7) nd = diags.Count; 8) if (nd >= 0) 9) { 10) return diags.IndexOf(elem); 11) } 12) return -1; 13) } 02.04.2016 ИСП РАН 38
  37. Как с помощью этого искать использование null? 1) int Index(IList<string>

    diags, string elem) 2) { 3) int nd; // diags:= xd 4) if (diags == null) // diags:= xd , [xd = null] 5) nd = 0; // diags:= xd , nd:= 0 [xd = null] 6) else 7) nd = diags.Count; 8) if (nd >= 0) 9) { 10) return diags.IndexOf(elem); 11) } 12) return -1; 13) } 02.04.2016 ИСП РАН 39
  38. Как с помощью этого искать использование null? 1) int Index(IList<string>

    diags, string elem) 2) { 3) int nd; // diags:= xd 4) if (diags == null) // diags:= xd , [xd = null] 5) nd = 0; // diags:= xd , nd:= 0 [xd = null] 6) else 7) nd = diags.Count; 8) if (nd >= 0) // diags:= xd , nd:= 0 [xd = null, 0>= 0] 9) { 10) return diags.IndexOf(elem); 11) } 12) return -1; 13) } 02.04.2016 ИСП РАН 40
  39. Как с помощью этого искать использование null? 1) int Index(IList<string>

    diags, string elem) 2) { 3) int nd; // diags:= xd 4) if (diags == null) // diags:= xd , [xd = null] 5) nd = 0; // diags:= xd , nd:= 0 [xd = null] 6) else 7) nd = diags.Count; 8) if (nd > 0) // diags:= xd , nd:= 0 [xd = null, 0 > 0] 9) { 10) return diags.IndexOf(elem); 11) } 12) return -1; 13) } 02.04.2016 ИСП РАН 41
  40. Как решить предикат пути? • Для решения используются специальные сторонние

    программы: SMT-решатели (Z3, stp, etc.). • Полученные формулы из символьных значений можно практически «как есть» отдавать решателям. • Если есть решение, то решатель его явно приведет, если его нет, то скажет, что формула несовместна. 02.04.2016 ИСП РАН 42
  41. Как добиться масштабируемости анализа? • Проблема: даже если не учитывать

    циклы, путей выполнения функции экспоненциально много. • Если в функции 10 операторов If, то через них проходит 1024 различных пути. • С учетом циклов, путей выполнения становится бесконечно много. 02.04.2016 ИСП РАН 43
  42. Можно объединять символьные состояния if (p == null) c =

    1; // p:= xp , c:= 1, …, [… && xp = null], else c = 0; // p:= xp , c:= 0, …, [… && xp != null], // p:= xp , c:= (xp =null ? 1: 0), [… && (xp !=null || xp =null), Итого 1024 возможных пути превращаются в 10 объединений. 02.04.2016 ИСП РАН 44
  43. Анализ циклов На практике, для получения хороших результатов достаточно анализировать

    лишь несколько итераций цикла. Как следствие, работа с массивами и коллекциями поддерживается только частично. 02.04.2016 ИСП РАН 45
  44. Использование указателя после сравнения с null в цикле. public int

    IndexOf(T item, IList<int> list) { for (int i = fromIndex, fakeIndex = 0; i < toIndex; i++, fakeIndex++) { var current = list[i]; if (current == null && item == null) return fakeIndex; if (current.Equals(item)) return fakeIndex; } return -1; } 02.04.2016 ИСП РАН 46
  45. Использование нулевого указателя: нужно поддерживать вызовы функции if (fcontext ==

    null) { // some code here } else { // some code there } vs.CreateWeight(fcontext, weightSearcher); ------------------------------------------ public override void CreateWeight(IDictionary context, IndexSearcher searcher) { Weight w = searcher.CreateNormalizedWeight(q); context[this] = w; } 02.04.2016 ИСП РАН 47
  46. Как организовать межпроцедурный анализ? • Стандартный подход: при вызове функции

    начинать её символьное выполнение с текущим символьным состоянием. • Плюс: точно • Минус: очень медленно • Альтернативный подход: заранее построить «резюме» для всех вызванных функций и использовать их вместо повторного анализа: • Плюс: достаточно быстро • Минус: неточно 02.04.2016 ИСП РАН 48
  47. Какую информацию содержит резюме? string Foo(IList<int> list, out string str,

    TextWriter wr) • К объекту wr будет обращение, если он не null • К объекту list будет обращение, если wr != null • После выполнения вызова, str гарантированно не null. • Функция может вернуть null • Функция может бросить IOException 02.04.2016 ИСП РАН 49
  48. Что такое граф вызовов? public void Baz() { } public

    void Main() { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 50 Main Foo Bar Baz
  49. Обратный топологический порядок public void Baz() { } public void

    Main() { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 51 Main Foo Bar Baz
  50. Порядок анализа public void Baz() { } public void Main()

    { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 52 Main Foo Bar Baz Резюме
  51. Порядок анализа public void Baz() { } public void Main()

    { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 53 Main Foo Bar Baz Резюме Резюме
  52. Порядок анализа public void Baz() { } public void Main()

    { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 54 Main Foo Bar Baz Резюме Резюме Резюме
  53. Порядок анализа public void Baz() { } public void Main()

    { Foo(); Bar(); } public void Bar() { Baz(); } public void Foo() { Bar(); Baz(); } 02.04.2016 ИСП РАН 55 Main Foo Bar Baz Резюме Резюме Резюме Резюме
  54. Проблемы при построении графа вызовов • Виртуальные вызовы • Вызовы

    делегатов • Вызовы внешних функций • Рекурсия 02.04.2016 ИСП РАН 56
  55. Схема работы анализатора 02.04.2016 ИСП РАН 57 Построение графа вызовов

    Граф вызовов Анализ функций в обратном топологическом порядке Ошибки Фильтрация и выдача ошибок
  56. Пример: Won’t fix public string Foo(object obj) { return (obj

    as Bar).Baz(); } 02.04.2016 ИСП РАН 58
  57. При анализе неизвестен контракт public string Foo(IList<string> list) { int

    max = -1; string maxString = null; foreach (var elem in list) { if (elem.Length > max) { max = elem.Length; maxString = elem; } } return maxString.ToUpper(); } 02.04.2016 ИСП РАН 59
  58. При анализе неизвестен контракт public string Foo(IList<string> list) { int

    max = -1; string maxString = null; foreach (var elem in list) { if (elem.Length > max) { max = elem.Length; maxString = elem; } } return maxString.ToUpper(); } 02.04.2016 ИСП РАН 60
  59. При анализе неизвестен контракт public string Foo(IList<string> list) { Debug.Assert(list.Any())

    int max = -1; string maxString = null; foreach (var elem in list) { if (elem.Length > max) { max = elem.Length; maxString = elem; } } return maxString.ToUpper(); } 02.04.2016 ИСП РАН 61
  60. Причины ложных срабатываний • Неточности в резюме функций; • Наличие

    неизвестных вызовов; • Наличие неявных контрактов функций; • Наличие инвариантов классов. 02.04.2016 ИСП РАН 62