Por Filipe Xavier de Oliveira

Introdução

Este artigo trata dos inteiros em C. Partiremos dos princípios básicos sobre inteiros, até chegarmos ao estudo de vulnerabilidades que envolvem o tema. Entre Janeiro e Agosto de 2022, o MITRE já possui registrados 96 CVEs (vulnerabilidades e exposições comuns) envolvendo inteiros. Portanto, esse é um tema que exige atenção. Conhecer com mais profundidade o funcionamento dos inteiros em C torna possível detectar falhas dessa natureza em aplicações reais. Sendo assim, para que se possa realizar uma análise sobre bugs na aritmética de inteiros, é preciso, sobretudo, estudarmos regras de conversão, wraparound e promoções de inteiros; além dos conceitos de overflow e underflow acerca do tema.

Inteiros

A definição matemática diz que um inteiro, do Latim integer, que significa o todo ou intacto, é um número natural que pode ser representado por qualquer valor positivo ou negativo, inclusive o zero, de modo que os inteiros nunca podem ser representados de forma fracionada. Por exemplo, 1, 5, 20190 são números inteiros, enquanto que 4.5 e ½ são exemplos de números não inteiros. Sendo assim, igualmente à matemática, na computação, os inteiros possuem essas mesmas características.

Tipos de inteiros

Existem 5 tipos básicos de inteiros. Eles são: char, short int, int, long int e long long int. Além disso, para cada um destes tipos, existe sua versão unsigned. Um tipo de inteiro signed é aquele que permite receber valores negativos, possibilitando, portanto, o uso de sinal. Consequentemente, o inteiro unsigned é aquele que suporta apenas valores positivos, partindo do número zero até o seu valor máximo. Há ainda os EXTENDED SIGNED INTEGER TYPES. Porém, como estes são definidos pela implementação, não iremos tratar deles neste estudo.

Tamanho declarado

Para cada um dos 10 tipos de inteiros, existe um valor máximo e mínimo atribuído. Como esse valor pode variar a depender da implementação, é recomendado utilizar o header <limits.h>, que é capaz de prover valores máximos e mínimos para vários tipos de inteiros. Portanto, é importante utilizá-lo para evitar problemas de portabilidade. Não se deve definir os valores máximos como constantes em seu código, pois a implementação pode considerar um valor maior ou menor do que o esperado.

Além disso, cabe ao compilador prover os valores máximos e mínimos. Desse modo, vejamos na tabela abaixo, todos os tipos de inteiros com seus respectivos valores atribuídos, bem como a caracterização quanto ao uso ou não do sinal negativo.

Tabela 1: intervalos de valores para tipos inteiros. Fonte: próprio autor.

Declarando variáveis

Como se pode perceber na tabela acima, por padrão, quando o sinal não está implícito considera-se o tipo como signed. Desse modo, os inteiros do tipo unsigned, (a não ser que seja declarado explicitamente), precisam ser diferenciados com o uso da palavra reservada unsigned, antecedendo o tipo no momento da sua declaração. Há, porém, uma exceção: especificamente para o tipo char, deve-se explicitar seu tipo signed, posto que é preciso diferenciar o signed char do plain char, devendo este último ser declarado apenas como char. Portanto, diferente das outras representações de inteiros, o char refere-se ao uso de um único caractere, enquanto que signed char passa a ser o único tipo signed que precisa do termo para lhe caracterizar como tal. Lembrando que um signed char ocupa o mesmo espaço em memória que um plain char. Vejamos as formas possíveis de declarar inteiros:

int X; //A palavra reservada signed pode ser omitida.
unsigned int Y; //A palavra reservada unsigned não pode ser omitida.
unsigned Z; //A palavra reservada int pode ser omitida.

Trecho de código 1: Exemplos de declaração de inteiros.

Perceba também que é possível omitir a palavra reservada int da declaração, quando outro tipo de sinalização ocorrer. Porém, caso haja somente o int na declaração da variável, é obrigatório o seu uso.

Unsigned integers

A utilização de inteiros unsigned é bastante comum para representar variáveis “contadoras” nos programas, uma vez que estes são semanticamente representados apenas pelos inteiros não negativos. Em contrapartida, um inteiro signed de mesmo tamanho (4 bits, por exemplo) divide a sua capacidade de representação entre valores positivos, valores negativos e o zero. Com isso, é trivial verificar que, em inteiros de mesmo tamanho, aqueles assinados como unsigned são capazes de armazenar valores positivos maiores do que aqueles assinados como signed.

A título de ilustração, vejamos a seguir todos os unsigned, conforme as especificações do Microsoft C++ para 32 bits e 64 bits:

Tabela 2: Inteiros unsigned para o Microsoft C++. Fonte: próprio autor.

Wraparound

O wraparound acontece quando uma variável do tipo unsigned ultrapassa seu limite máximo ou mínimo, tipicamente como consequência de uma operação aritmética. No código abaixo é possível visualizar 2 exemplos de wraparound ao realizar, respectivamente, um incremento na variável varWrapMax e um decremento na variável varWrapMin:

#include <stdio.h>
#include <limits.h>

int main() {
  unsigned int varWrapMax = UINT_MAX;  // 4,294,967,295 no x86
  unsigned int varWrapMin = 0;

  printf("Example 1: \n");
  printf("varWrapMax = %u\n", varWrapMax); //varWrapMax = 4,294,967,295
  varWrapMax++;
  printf("varWrapMax = %u\n\n", varWrapMax); //varWrapMax = 0
  
  printf("Example 2: \n");
  printf("varWrapMin = %u\n", varWrapMin); //varWrapMin = 0
  varWrapMin--;
  printf("varWrapMin = %u\n\n", varWrapMin); //varWrapMin = 4,294,967,295

  return 0;
}

Trecho de código 2: Exemplos de wraparound.

Ok, leitor, é bem possível que neste momento você esteja pensando: “pô, mas isso aí é um integer overflow, por que você está chamando de wraparound?”. O questionamento é válido, mas levemente impreciso. Diferente do que se imagina, computações envolvendo operadores unsigned não geram overflow. É contraintuitivo, mas o exemplo de wraparound é na verdade um well-defined behavior. Segue um trecho da especificação do C padrão:

A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.

É válido pontuar, porém, que, embora seja considerado um well-defined behavior, o wraparound, exemplificado, acima pode gerar comportamentos inesperados em programas escritos em C.

Outros exemplos

Como é de se esperar, não são apenas as operações de incremento (++) e decremento (–) que geram casos de wraparound nos inteiros. Na verdade, pelo menos 11 operadores podem gerar um wraparound de inteiros unsigned, como ilustrado na tabela a seguir:

Tabela 3: Operadores e geração de wraparound em C. Fonte: próprio autor.

Dada a tabela acima, vejamos alguns exemplos onde ocorrem wraparound.

Exemplo #01 – Adição de inteiros

Como previamente ilustrado na Tabela 3, a adição de inteiros é uma operação onde há uma chance de wraparound. O trecho de código a seguir exemplifica o cenário descrito:

#include <stdio.h>

int main() {
  unsigned char a = 0;
  a = 0xe1;
  a = a + 0x25;
  printf("a = %u\n", a);
  return 0;
}

Trecho de código 3: Wraparound na adição de inteiros.

Após a adição, o valor do unsigned char deve ser de 0x106 (ou 262 em decimal), certo? Tente compilar esse código e imprimir a resposta aí no seu computador, prometo que eu espero… Algo estranho não é? A intuição nos diz que o resultado a ser exibido deveria ser 262, mas, na verdade, foi exibido 6 e nenhum erro impediu a execução do programa. O motivo? Trata-se de um tipo cuja capacidade de armazenamento varia no intervalo entre 0 e 255 e a especificação determina um wraparound. Logo, seguindo a fórmula especificada no standard do C, temos:


0x106h % (0xFF + 0x1) == 0x106h % 0x100h = 0x6h (em hexadecimal).
262d % (255 +1) == 262d %256d = 6d (em decimal).

Exemplo #02 – Subtração de inteiros

De maneira análoga a adição, a subtração também gera um wraparound quando ultrapassado o limite inferior àquele presente na definição do inteiro unsigned. O trecho de código que segue ilustra um exemplo de wraparound na subtração da variável a.

#include <stdio.h>

int main() {
  unsigned char a;
  a = 0x0;
  a = a - 0x1;
  printf("a = %u\n", a);
  return 0;
}

Trecho de código 4: Wraparound na subtração de inteiros.

Nesse caso, temos 0x0h – 0x1h. Como a, por ser definido como unsigned, não pode receber um valor negativo, ele realiza um wraparound para 0xFF. Ao usarmos a fórmula 0xFFh % 0x100h = 0xFFh, temos o equivalente a 255d em decimal, ou seja, a um UCHAR_MAX.

Exemplo #03 – Multiplicação de inteiros

Vejamos um último exemplo, a multiplicação de inteiros. Segue o código:

#include <stdio.h>

int main() {
  unsigned char a;
  a = 0xe1;
  a = a * 0x25;
  printf("a = %u\n", a);
  return 0;
}

Trecho de código 5: wraparound na multiplicação de inteiros.

Aqui, repetindo os exemplos já citados, o resultado do wraparound dá-se com 0x2085h % 0x100h = 0x85h ou, simplesmente, 133 em decimal.

Signed Integers (números inteiros assinados)

Inteiros signed são usados para representar valores negativos, zero e valores positivos. Como visto anteriormente, excluindo-se o _Bool, há um inteiro signed para cada tipo. Variáveis declaradas como _Bool podem armazenar somente os valores 0 e 1. Além disso, a distância entre os números negativos e positivos vai depender do tipo de representação.

Historicamente, em C, é comum ocorrerem 3 tipos de representações para os tipos signed int: a) magnitude e sinal; b) complemento de um, e; c) complemento de dois. Vejamos cada uma dessas representações.

  1. Na representação de sinal e magnitude, o bit mais significativo indica o sinal, de modo que o restante dos bits representa a magnitude do valor em uma notação binária.
  2. No complemento de um, o valor é obtido através da inversão de todos os bits na representação binária do número inteiro em questão. Em outras palavras, transformando bit 0 para 1, e o bit 1 para 0.
  3. No complemento de dois (a representação que também ocorre em forma binária) o bit mais significativo representa o sinal. O bit mais significativo valorado com zero ou com 1, representa respectivamente os sinais positivo e negativo. Os bits restantes são utilizados para representar a magnitude. Tal como no complemento de um, no complemento de dois, é realizado uma inversão de todos os bits e por fim será acrescido mais 1 ao bit menos significativo.

Cabe lembrar que a representação não pode ser escolhida, posto que ela é determinada pelo tipo de implementação utilizada. Atualmente, a representação mais utilizada é o complemento de dois. Portanto, vamos assumir esse tipo de representação adiante.

Inteiros signed de tamanho N podem ter sua representação expressa no intervalo de -até . Desse modo, utilizando um signed char de 8 bits como exemplo, temos um range de -128 até 127.

Ainda é preciso definir dois termos importantes, Overflow e Underflow. Enquanto Overflow representa um valor que excede o valor máximo de um determinado tipo, Underflow representa um valor menor que o valor mínimo permitido.

Para o standard, um Overflow em operações de inteiro com sinal representa um undefined behaviour. Por isso, é importante garantir que operações com sinal nunca causem um Overflow ou Underflow.

Abaixo, segue uma tabela dos operadores que podem vir a causar um overflow em operações signed:

TABELA 4: Garanta que operações em SIGNED INT não resultem em overflow. Fonte: Atlassian.

Abaixo, dois casos simples de Overflow e Underflow em inteiros:

Exemplo #04 – Adição

int x = 0x7FFFFFFF;
printf("%d\n", x);
x = x + 1;
printf("%d\n",x);

Trecho de código 6: Soma de Unsigned.

Nota-se que a soma acima resulta num overflow que ultrapassa o limite máximo permitido, desse modo, o resultado será 0x80000000 que é um número negativo. Sendo assim, aqui está o resultado:

2147483647
-2147483648

Exemplo #05 – Subtração

int x = 0x80000020;
printf("%d\n", x);
x = x - 200;
printf("%d\n", x);

Trecho de código 7: Subtração de Unsigned.

Já no caso acima, a subtração causa o underflow, pois o valor ficará abaixo de 0x800000000 resultando num número positivo. Abaixo, o resultado:

-2147483616
2147483480

Desse modo, percebemos que os inteiros unsigned possuem um comportamento bem definido nos casos de wraparound. Por outro lado, vemos que os inteiros signed que geram overflow, ou o possibilitam, devem sempre ser considerados um defeito.

Conversão aritmética

 Conversões podem ocorrer, explicitamente, como o resultado de um cast; ou, implicitamente, através de uma operação. No caso de uma conversão explícita, ela ocorre por demanda de quem escreveu aquele trecho de código. Vejamos o exemplo abaixo:

int x = 10;
long z = (long)x;

Trecho de código 8: Cast.

Um cast nada mais é que o nome do tipo entre parênteses antes de qualquer expressão. O cast converte o tipo original para o tipo desejado. No exemplo acima, convertemos o int x para long através do cast (long). Nesse caso, não encontramos qualquer problema, pois um int cabe dentro do intervalo de representação do tipo long. Contudo, os problemas começam a acontecer quando tipos menores extrapolam o tamanho em tipos maiores, ou o que é ainda pior, quando tudo isso ocorre sem o controle ou a permissão do programador.

Quanto à conversão implícita, ela acontece quando operações de tipos diferentes ocorrem. Além disso, ressaltamos que as regras que determinam os valores a serem convertidos implicitamente tendem a ser complicadas. Mesmo porque, geralmente, elas envolvem os conceitos de rank (classificação) de conversão e promoções de inteiros, os quais estudaremos a seguir.

Rank (classificação) de conversão

Cada tipo de inteiro possui um rank de conversão que vai determinar quando e como as conversões serão realizadas. De acordo com o standard (padrão), existem oito regras:

  1. o rank de um inteiro signed é maior que o rank de qualquer inteiro signed com menos precisão – o número de bits usado para representar valores, excluindo o sinal e o padding;
  2. o rank de long long int é maior que qualquer long int, que é maior que qualquer int, que por sua vez é maior que qualquer short int, sendo este maior que qualquer signed char;
  3. o rank de qualquer unsigned é equivalente ao rank do seu correspondente signed;
  4. o rank de um char é equivalente ao rank de um signed char e um unsigned char;
  5. o rank de _Bool é menor que qualquer outro rank de qualquer outro tipo de inteiro;
  6. o rank de qualquer tipo enum é equivalente ao rank de tipo compatível. Cada tipo enum é compatível com char, um inteiro signed ou um inteiro unsigned;
  7. dois inteiros signed não terão o mesmo rank, mesmo que eles possuam a mesma representação;
  8. o rank de qualquer inteiro estendido, relativo a outro inteiro estendido, e com a mesma precisão, será definido pela implementação.

Promoções de inteiro

Promoção é o processo de converter small types em um int ou unsigned int. Sendo small type, um inteiro que possui um rank de conversão mais baixo que o de um int ou unsigned int.

Existem dois motivos pelos quais a existência das promoções é justificada. O primeiro motivo deve-se às otimizações do processador, já que trabalhar somente com tipos inteiros é sempre mais rápido. Consequentemente, o outro motivo é que, assim, é possível evitar overflow em operações de small types com tipos maiores.

Para cada tipo em C/C++, existe um alinhamento requerido, o que é mandatório pelas arquiteturas de processadores. Para não abordarmos outros temas, é preciso apenas ter em mente que, para boa parte dos processadores, é mais econômico ler todos os 4 bytes de um inteiro em um ciclo de memória, do que por exemplo, ler 1 byte a cada ciclo. Por isso, a preferência em transformar os small types em inteiros.

Caso um inteiro signed possa representar todos os valores do tipo original, o valor é convertido para inteiro; do contrário, o valor será convertido para unsigned.

Exemplo #06

char a = 30, b = 40, c = 10;
char d = (a * b) / c;
printf ("%d ", d);
return 0;

Trecho de código 9: Conversão Ar.

Realizando as operações manualmente, vemos que d = 30 * 40 = 1200, de modo que o valor de um char (que é de -128 até 127) é ultrapassado.

Compilando o código acima e o executando, vemos o seu output (resultado):

output:

120

Como pode o resultado ser 120? A resposta é que o compilador realizou uma promoção do small type char para inteiro no momento da multiplicação. Logo, 1200 é um valor que cabe dentro de um inteiro. Por essa razão, a divisão ocorre normalmente, sem erros de compilação ou crash no programa. O que ocorreu foi a divisão de inteiro por um char, desse modo, não enfrentamos problemas na hora da execução.

Exemplo #07

signed char result, a, b,  c;
a = 100; b = 3; c = 4;
result = a * b / c;

Trecho de código 10: Promoção.

Como resultado, temos que a * b = 300, excedendo o valor máximo de um signed char, que é 127. Porém, devido à promoção, o resultado final será 75, e 75 está dentro do range de um signed char.

Para compreender as conversões aritméticas, é preciso entender os operadores e as condições das conversões. A primeira operação da conversão aritmética é checar se o tipo float foi utilizado dentro da operação. Depois disso, as seguintes regras são aplicadas:

  1. se um tipo de um lado dos operandos é long double, o outro operando será convertido para long double;
  2. caso contrário, se um tipo de um dos operandos é double, o outro operando será convertido para double;
  3. caso contrário, se um tipo de um lado do operando é float, o outro operando será convertido para float;
  4. caso contrário, promoções de inteiro serão realizadas em ambos os operandos.

Por exemplo, se um operando é do tipo double e o outro operando é do tipo int, o operando int é convertido para um do tipo double.

As seguintes regras são aplicadas quando não tratamos de tipos float:

  1. quando ambos os operandos possuem o mesmo tipo, nenhuma conversão é necessária;
  2. caso contrário, quando ambos os operandos têm o mesmo sinal, o tipo que tiver menor rank na conversão é convertido para o tipo com maior rank;
  3. quando o operando é do tipo unsigned e tem o rank maior ou igual ao outro operando, então o operando com o tipo signed é convertido para o tipo unsigned;
  4. quando o operando do tipo signed representa todos os valores de um operando do tipo unsigned, o operando do tipo unsigned será convertido para signed. Por exemplo, se um operando é do tipo unsigned int e o outro operando é do tipo signed long long, caso o signed long long possa representar todos os valores do unsigned int, logo o unsigned int será convertido para um objeto do tipo signed long long;
  5. caso contrário, ambos operandos são convertidos para unsigned respeitando o tipo do operando com sinal.

A seguir, veremos alguns exemplos de conversão implícita.

Exemplo #08

char a = 0xfb;
unsigned char b = 0xfb;
printf("a = %c", a);
printf("\nb = %c", b);
 if (a == b)
   printf("\nValores iguais");
 else
   printf("\nValores diferentes");

Trecho de código 11.

Quando imprimimos na tela a e b, o mesmo caractere é impresso, porém quando nós os comparamos, temos resultados diferentes, apesar de ambos serem representados como char.

output:

Valores diferentes

Por que o output desta comparação resulta em valores diferentes?

De acordo com as regras vistas, signed char e unsigned char possuem o mesmo rank de conversão. Porém, são small types; logo, serão convertidos para int. Como um signed int comporta os valores promovidos, tanto o valor de a quanto o valor de b serão convertidos para signed int no momento da comparação. Portanto, a será igual a -5 e b será igual a 251, seguindo as regras do wraparound. Logo, eles não são iguais, apesar de possuírem os mesmos valores atribuídos.

Exemplo #09

unsigned char a = 255;
unsigned char b = 255;
unsigned char c = a + b;

if (c > 300)
    printf("Success!\n");
else
    printf("Fail!\n");

Trecho de código 12.

Neste caso, c será igual a 255 + 255 = 510. O resultado já transbordaria o valor máximo de UCHAR_MAX. Porém, devido a promoção do char para inteiro, nenhum erro ocorreu. Entretanto, no momento da comparação, o valor de c será reduzido ao módulo do valor máximo do tipo mais 1 (510 % 255 + 1 = 254). Logo, nessa condição, o resultado será 255 + 255 = 254.

output:

Fail!

Exemplo #10

int len = -5;
if (len < sizeof(int))
    printf("x\n");

Trecho de código 13 .

Para melhor compreensão desse exemplo, é preciso saber que o tipo retornado pelo operador sizeof, é o tipo size_t, que por sua vez é do tipo unsigned int. Variáveis do tipo size_t, geralmente, garantem precisão suficiente em relação ao tamanho de um objeto. O limite de size_t é definido pela macro SIZE_MAX. Assim, temos uma comparação de um inteiro signed com um inteiro unsigned.

Lembremos a regra de conversão número 3, que diz: “Quando o operando é do tipo unsigned e tem o rank maior ou igual ao outro operando, então o operando com o tipo signed é convertido para o tipo unsigned.” Logo, a variável len será convertida para unsigned, assumindo um valor de 4294967291.

A comparação que acontece de fato, é essa:

if ( 4294967291 < sizeof(int))

Trecho de código 14.

Exemplo #11

char *buf;
int   len = -1;
if (len > 8000) { return 0; }
buf = malloc(len);
read(0, buf, len); 
return 0;

Trecho de código 15.

Muitas bibliotecas e suas respectivas funções usam algum parâmetro para medir o tamanho de um objeto, geralmente, utilizam-se de um argumento que é do tipo size_t, por exemplo, como a função read que possui a seguinte estrutura:

size_t read (int fd, void* buf, size_t cnt);

Trecho de código 16.

Sendo o terceiro parâmetro aquele que determina o tamanho a ser lido, vemos que, no exemplo acima, o parâmetro len, vai se tornar do tipo unsigned. Logo, é possível causar um overflow no ato da leitura, pois seria possível ler mais bytes que o programado.

Desse modo, é importante redobrar a atenção ao se valer de um size como parâmetro de uma função, pois, geralmente, esse parâmetro será convertido implicitamente para unsigned int.

Abaixo, temos uma lista de algumas funções que utilizam como size o tipo size_t:

ssize_t read(int fd, void *buf, size_t count);
void *memcpy(void *dest, const void *src, size_t n);
void *malloc(size_t size);
int snprintf(char *s, size_t size, const char *fmt, ...);
char *strncpy(char *dest, const char *src, size_t n);
char *strncat(char *dest, const char *src, size_t n);

Trecho de código 17.

Exemplo #12

int read_user_data(int sockfd) {
   int length, sockfd, n;
   char buffer[1024];
   length = get_user_length(sockfd);
   if (length > 1024)
      return -1;
   if (read(sockfd, buffer, length) < 0)
      return -1
   return 0;
}

Trecho de código 18.

Repetindo o que vimos no exemplo 11, é possível perceber a condição a ser burlada:

if (length > 1024)
   return -1;

Trecho de código 19.

Necessariamente, length deve estar dentro do limite [INT_MIN; 1024], e em seguida, no trecho de código, é utilizada a função read, usando com size o parâmetro length. Sendo assim, a função read espera um tamanho do size_t, que, por sua vez, é do tipo unsigned.

Então, é possível causar um stack overflow, caso seja inserido o número -1 como length: na primeira verificação -1 < 1024; em seguida, como length será transformado para unsigned int, assumirá o valor de 4294967295.

Exemplo #13

No exemplo abaixo, veremos que é possível causar um overflow, através da multiplicação de dois unsigned.

unsigned short a = 45000 , b = 50000;
unsigned int c = a * b;

Trecho de código 20.

O problema acontece quando a multiplicação ocorre, pois 45000 * 50000 = 2250000000. Naturalmente, esse valor excede o valor máximo de um short. Portanto, pelas regras de conversão, os unsigned short serão convertidos para signed int, que por sua vez, também terá o seu valor máximo excedido pela multiplicação. Logo, um overflow acontecerá em um produto de dois unsigned.

Exemplo #14

A descrição do bug CVE-2015-6575 é a seguinte:

SampleTable.cpp in libstagefright in Android before 5.1.1 LMY48I does not properly consider integer promotion, which allows remote attackers to execute arbitrary code or cause a denial of service (integer overflow and memory corruption) via crafted atoms in MP4 data

Buscando o trecho do código vulnerável, é possível observar o seguinte:

mTimeToSampleCount = U32_AT(&header[4]);
uint64_t allocSize = mTimeToSampleCount * 2 * sizeof(uint32_t);

Trecho de código 21.

Temos o objeto mTimeToSampleCount que recebe o valor atribuído através de um tipo unsigned_int32. Posteriormente, o objeto allocsize que é do tipo unsigned long long(cujo valor equivale ao de um unsigned_int64) terá seu valor atribuído através da multiplicação de dois objetos unsigned, pois mTimeToSampleCount é unsigned. Já que sizeof, como vimos anteriormente, é do tipo size_t, que, por sua vez, é do tipo unsigned.

Considerando o que vimos, anteriormente, a promoção ocorrerá e, caso o usuário insira um valor muito grande, acontecerá o overflow.

A correção sugerida para esse tipo de cenário de multiplicação de inteiros unsigned é forçar uma conversão explícita.

E para finalizar, é possível visualizar a correção aplicada na biblioteca do Android:

uint64_t allocSize = mTimeToSampleCount * 2 * (uint64_t)sizeof(uint32_t);

Trecho de código 22.

Sendo assim, concluímos nossos exemplos, bem como esse estudo, verificando, neste caso, que, com uso do cast uint64_t, a multiplicação será realizada em um range maior, e que comporte os valores inseridos. Naturalmente, isso é uma parte da correção, pois é importante validar condições pós multiplicação, a fim de evitar a utilização de valores não esperados.

Conclusão

Com tudo isso, ressaltamos a importância do estudo atento e aprofundado dos inteiros. Pois acreditamos que a chave para prevenir vulnerabilidades como as que vimos, está no entendimento das nuances de comportamento dos inteiros, bem como em sua aplicação e implementação nos sistemas.

Referências

SEACORD, Robert C. Secure Coding in C and C++, 2ª Edição 2013. Addison-Wesley Professional.

SEACORD, Robert C. Effective C An Introduction to Professional C Programming. 2020. NoStarch.

DOWD, Mark; MCDONALD, John; SCHUH, Justin. The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities. 2006. Addison-Wesley Professional.

ISO/IEC 9899:2011. INT30-C. Ensure that unsigned integer operations do not wrap. Disponível em <https://wiki.sei.cmu.edu/confluence/display/c/INT30-C.+Ensure+that+unsigned+integer+operations+do+not+wrap>.