Вебинар: Механизмы в SAST-решениях для выявления дефектов из OWASP Top Ten - 12.03
В этой статье расскажем, как можно ошибиться при использовании '^' для возведения в степень. Хотя в ряде языков он отвечает именно за возведение в степень, во многих популярных стеках разработки этот оператор выполняет операцию исключающего ИЛИ (XOR). Мы разберём, к каким последствиям приводит подобная путаница, и покажем реальный пример этой ошибки в механизме работы очереди внутри популярной библиотеки.

Во многих современных языках программирования оператор ^ отвечает за побитовое исключающее ИЛИ (XOR); к таким языкам относится Golang. Однако в некоторых языках этот оператор отвечает за возведение в степень — среди них Lua, VB.NET, Julia, R и другие. Более того, в повседневной жизни люди зачастую используют именно^ для обозначения возведения числа в степень. И это как будто логично — стрелочкой мы как бы указываем на место, где должен быть показатель степени числа.
Поэтому возможна ситуация, когда разработчик для возведения в степень двойки использует исключающее ИЛИ ^ вместо побитового сдвига <<. Может показаться, что ошибка надуманная, и в действительности разработчики так не ошибаются. Однако мы все же решили добавить соответствующее диагностическое правило про использование XOR вместо побитового сдвига для нашего Golang-анализатора, вдохновившись аналогичным из CodeQL.
О том, как сделать собственный анализатор для Go, мы писали в статье "Как сделать свой статический анализатор для Go?".
Также мы наткнулись на обсуждение на официальном сайте GCC. В нём поднимается вопрос о том, что данный паттерн действительно зачастую является ошибочным и стоит выдавать предупреждения на те места, где случайно может быть использован ^ вместо <<.
Свой анализатор PVS-Studio мы тестируем не только на синтетических примерах, но и на реальных проектах. Для этого у нас есть выборка открытых проектов с фиксированными версиями. Но для данной ошибки нашей выборки оказалось мало, поэтому мы использовали свою утилиту, которая скачивает и анализирует свыше 1000 проектов на Go с GitHub. Однако мы не ожидали встретить ошибку, связанную с использованием ^ в качестве оператора возведения в степень на реальных крупных проектах.
Но, судя по тому, что вы читаете эту статью, нам есть чем с вами поделиться.
Как уже упоминалось, ошибка заключается в том, что разработчик использовал ^ вместо <<:
func main() {
fmt.Println(2 ^ 32)
}
Распознать её достаточно просто: слева от оператора исключающего ИЛИ находится 2, поскольку разработчик пытается получить какую-то степень двойки.
Однако корректный способ вычислить 2 в степени 32 выглядит так: 1 << 32.
А теперь перейдём к рассмотрению ошибок на реальных проектах.
Проект Lancet позиционируется как комплексная и эффективная библиотека с различными вспомогательными функциями и структурами на языке Go. Но мы заинтересовались им, потому что наш анализатор PVS-Studio нашёл следующую ошибку:
func (q *ArrayQueue[T]) Enqueue(item T) bool {
if q.tail < q.capacity {
q.data = append(q.data, item)
// q.tail++
q.data[q.tail] = item
} else {
//upgrade
if q.head > 0 {
....
} else {
if q.capacity < 65536 {
if q.capacity == 0 {
q.capacity = 1
}
q.capacity *= 2
} else {
q.capacity += 2 ^ 16 // <=
}
tmp := make([]T, q.capacity, q.capacity)
copy(tmp, q.data)
q.data = tmp
}
q.data[q.tail] = item
}
q.tail++
q.size++
return true
}
Предупреждение PVS-Studio: V8014 Suspicious use of the bitwise XOR operator '^'. The exponentiation operation may have been intended here. arrayqueue.go 87
Из контекста не сложно понять, что речь идёт про операцию добавления элемента в очередь. Суть проста: если вместимость очереди позволяет добавить туда элемент – мы его добавляем, а если места не хватает, то нужно либо перераспределить места в очереди, либо увеличить вместимость.
Из кода видно, что если вместимость меньше 65536 (а это 2 в 16 степени), то удваиваем вместимость очереди. А если вместимость уже больше 65536, то мы должны увеличить её на 2 в 16 степени, но из-за использования XOR будет добавлено 18 (2 ^ 16).
К чему это может привести? К тому, что такая очередь будет работать неэффективно. Вместимость увеличивается на незначительное значение, поэтому расширять её приходится чаще. И соответственно чаще придётся копировать все элементы из старой очереди в новую с увеличенным размером.
Дополнительный признак ошибки — сама запись 2 ^ 16, намного проще сразу написать 18, если так и задумывалось.
Давайте рассмотрим ещё одно срабатывание этого диагностического правила. В этот раз ошибка обнаружена в проекте Calico.
func parsePorts(portsStr string) *bitset.BitSet {
setOfPorts := bitset.New(2 ^ 16 + 1) // <=
for _, p := range strings.Split(portsStr, ",") {
if strings.Contains(p, "-") {
// Range
parts := strings.Split(p, "-")
low, err := strconv.Atoi(parts[0])
if err != nil {
panic(err)
}
high, err := strconv.Atoi(parts[1])
if err != nil {
panic(err)
}
for port := low; port <= high; port++ {
setOfPorts.Set(uint(port))
}
} else {
// Single value
port, err := strconv.Atoi(p)
if err != nil {
panic(err)
}
setOfPorts.Set(uint(port))
}
}
return setOfPorts
}
Предупреждение PVS-Studio: Suspicious use of the bitwise XOR operator '^'. The exponentiation operation may have been intended here. flattener.go 187
Здесь та же проблема — использование ^ вместо <<. Скорее всего, предполагалось число равное 2 в 16 степени вместо 18. Но к чему же это может привести? К тому, что длина BitSet будет куда меньше ожидаемой в результате инициализации с помощью bitset.New. И программа будет тратить время на расширение длины и перекопирование в методе Set посредством метода extendSet:
func (b *BitSet) Set(i uint) *BitSet {
if i >= b.length { // if we need more bits, make 'em
b.extendSet(i)
}
b.set[i>>log2WordSize] |= 1 << wordsIndex(i)
return b
}
func (b *BitSet) extendSet(i uint) {
if i >= Cap() {
panic("You are exceeding the capacity")
}
nsize := wordsNeeded(i + 1)
if b.set == nil {
b.set = make([]uint64, nsize)
} else if cap(b.set) >= nsize {
b.set = b.set[:nsize] // fast resize
} else if len(b.set) < nsize {
newset := make([]uint64, nsize, 2*nsize) // increase capacity 2x
copy(newset, b.set)
b.set = newset
}
b.length = i + 1
}
Казалось бы, не критично, но всех этих операций можно избежать, если изначально правильно задать длину для BitSet.
Возможно, подобная ошибка есть и в вашем проекте. Проверить это несложно – достаточно выполнить поиск по оператору ^, тем более что исключающее ИЛИ используется не так часто.
Если же вы хотите иметь возможность находить и устранять такие проблемы сразу же на этапе разработки, советуем обратить внимание на инструменты статического анализа.
Команда PVS-Studio уже много лет разрабатывает и поддерживает одноименный анализатор для C, C++, C# и Java. Также мы выпускаем анализатор для Golang в ранний доступ. Чтобы это не пропустить, рекомендуем посетить нашу страницу "Доступ к ранним версиям PVS-Studio".
Вот и всё. Мы рассмотрели ошибку, которую с одной стороны можно считать простой опечаткой, но с другой — она способна привести к серьёзным последствиям.
Чтобы обезопасить код стоит регулярно выполнять статический анализ кода. Он помогает выявлять небезопасные конструкции, потенциально опасные участки логики и ошибки, которые способны привести к проблемам безопасности приложения.
Берегите себя и свой код!
0