Nos artigos anteriores da série, discutimos segurança de memória e segurança de threads no Rust. Neste último artigo, veremos as implicações de um aplicativo Rust real usando o projeto CSS Quantum como exemplo.O mecanismo CSS aplica regras CSS à página. Este é um processo descendente que desce a árvore DOM. Após calcular o CSS pai, os estilos filhos podem ser calculados independentemente: ideal para computação paralela. Em 2017, a Mozilla fez duas tentativas para paralelizar o sistema de estilo usando C ++. Ambos falharam.
O desenvolvimento de CSS quântico começou a aumentar a produtividade. Melhorar a segurança é apenas um bom efeito colateral.
Há uma certa conexão entre proteção de memória e bugs de segurança da informação. Portanto, esperávamos que o uso do Rust reduzisse a superfície de ataque no Firefox. Este artigo analisará as possíveis vulnerabilidades que foram identificadas no mecanismo CSS desde o lançamento inicial do Firefox em 2002. Então olhe o que poderia e não poderia ter sido evitado com Rust.
Para sempre, 69 erros de segurança foram detectados no componente CSS do Firefox. Se tivéssemos uma máquina do tempo e pudéssemos escrevê-la Rust desde o início, 51 erros (73,9%) se tornariam impossíveis. Embora o Rust facilite a escrita de um bom código, ele também não oferece proteção absoluta.
Ferrugem
Rust é uma linguagem de programação de sistema moderna que é segura para tipos e memória. Como efeito colateral dessas garantias de segurança, os programas Rust também são seguros para threads em tempo de compilação. Assim, Rust é particularmente adequado para:
- processamento seguro de dados não confiáveis recebidos;
- concorrência para melhorar o desempenho;
- integração de componentes individuais na base de código existente.
No entanto, o Rust não corrige explicitamente algumas classes de erro, especialmente erros de correção. De fato, quando nossos engenheiros reescreveram o Quantum CSS, eles repetiram acidentalmente um bug crítico de segurança, que foi corrigido anteriormente no código C ++, e excluíram acidentalmente o
bug 641731 , que permite o vazamento da história global via SVG. O erro foi registrado novamente como
bug 1420001 . Um vazamento de histórico é classificado como uma vulnerabilidade de segurança crítica. A correção inicial foi uma verificação adicional para verificar se o documento SVG é uma imagem. Infelizmente, essa verificação foi perdida ao reescrever o código.
Embora os testes automatizados devam encontrar violações da regra
:visited
assim, na prática eles não encontraram esse erro. Para acelerar os testes automáticos, desativamos temporariamente o mecanismo que testou esse recurso - os testes não são particularmente úteis se não forem realizados. O risco de reimplementar erros lógicos pode ser reduzido por uma boa cobertura de teste. Mas ainda existe o perigo de novos erros lógicos.
À medida que um desenvolvedor se familiariza com Rust, seu código se torna ainda mais seguro. Embora o Rust não evite todas as vulnerabilidades possíveis, ele corrige toda uma classe dos bugs mais graves.
Erros de segurança CSS quânticos
Em geral, por padrão, o Rust evita erros relacionados à memória, limites, variáveis nulas / não inicializadas e estouros de número inteiro. O bug não padrão mencionado acima permanece possível: ocorre uma falha devido à falha na alocação de memória.
Erros de segurança por categoria
- Memória: 32
- Fronteiras: 12
- Implementação: 12
- Nulo: 7
- Estouro de pilha: 3
- Estouro de número inteiro: 2
- Outro: 1
Em nossa análise, todos os bugs estão relacionados à segurança, mas apenas 43 receberam uma classificação oficial (é atribuída pelos engenheiros de segurança da Mozilla com base em suposições qualificadas sobre "exploração"). Erros comuns podem indicar funções ausentes ou algum tipo de mau funcionamento, o que não leva necessariamente a vazamento de dados ou alteração de comportamento. Os erros oficiais de segurança variam de baixa importância (se houver uma forte restrição na superfície de ataque) a vulnerabilidade crítica (pode permitir que um invasor execute código arbitrário na plataforma do usuário).
As vulnerabilidades de memória são frequentemente classificadas como sérios problemas de segurança. Das 34 questões críticas / sérias, 32 foram relacionadas à memória.
Distribuição de gravidade de bugs de segurança
- Total: 70
- Erros de segurança: 43
- Crítico / Sério: 34
- Ferrugem fixa: 32
Comparando Rust e C ++
Bug 955913 - estouro de buffer de pilha na função
GetCustomPropertyNameAt
. O código usou a variável incorreta para indexação, o que levou à interpretação da memória após o final da matriz. Isso pode causar uma falha ao acessar um ponteiro incorreto ou copiar memória para uma seqüência de caracteres que é passada para outro componente.
A ordem de todas as
propriedades CSS (incluindo personalizada, ou seja, personalizada) é armazenada na matriz
mOrder
. Cada elemento é representado por um valor de propriedade CSS ou, no caso de propriedades customizadas, um valor que começa com
eCSSProperty_COUNT
(número total de propriedades CSS não customizadas). Para obter o nome das propriedades personalizadas, você primeiro precisa obter o valor de
mOrder
e, em seguida, acessar o nome no índice correspondente da matriz
mVariableOrder
, que armazena os nomes das propriedades personalizadas em ordem.
Código C ++ vulnerável:
void GetCustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const { MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT); aResult.Truncate(); aResult.AppendLiteral("var-"); aResult.Append(mVariableOrder[aIndex]);
O problema ocorre na linha 6 ao usar
aIndex
para acessar o elemento da matriz
mVariableOrder
. O fato é que o
aIndex
deve ser usado com a matriz
mOrder
, não o
mVariableOrder
. O elemento correspondente para a propriedade customizada representada por
aIndex
no
mOrder
é realmente
mOrder[aIndex] - eCSSProperty_COUNT
.
Código C ++ corrigido:
void Get CustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const { MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT); uint32_t variableIndex = mOrder[aIndex] - eCSSProperty_COUNT; aResult.Truncate(); aResult.AppendLiteral("var-"); aResult.Append(mVariableOrder[variableIndex]); }
Código de ferrugem correspondente
Embora o Rust seja um pouco semelhante ao C ++, ele usa outras abstrações e estruturas de dados. O código de ferrugem será muito diferente do C ++ (veja abaixo para mais detalhes). Primeiro, vamos ver o que acontece se o código vulnerável for traduzido da maneira mais literal possível:
fn GetCustomPropertyNameAt(&self, aIndex: usize) -> String { assert!(self.mOrder[aIndex] >= self.eCSSProperty_COUNT); let mut result = "var-".to_string(); result += &self.mVariableOrder[aIndex]; result }
O compilador Rust aceitará esse código porque o comprimento dos vetores não pode ser determinado antes da execução. Diferentemente das matrizes, cujo comprimento deve ser conhecido, o
tipo Vec no Rust tem um tamanho dinâmico. No entanto, a verificação de limites é incorporada à implementação do vetor de biblioteca padrão. Se um índice inválido aparecer, o programa será encerrado imediatamente de maneira controlada, impedindo qualquer acesso não autorizado.
O
código real CSS quântico usa estruturas de dados muito diferentes, portanto, não há equivalente exato. Por exemplo, usamos as poderosas estruturas de dados integradas da Rust para unificar o arranjo e os nomes das propriedades. Isso elimina a necessidade de manter duas matrizes independentes. As estruturas de dados de ferrugem também melhoram o encapsulamento de dados e reduzem a probabilidade de tais erros lógicos. Como o código deve interagir com o código C ++ em outras partes do navegador, a nova função
GetCustomPropertyNameAt
se parece com o código idiomático Rust. Mas ainda oferece todas as garantias de segurança, além de fornecer uma abstração mais compreensível dos dados subjacentes.
tl; dr
Como as vulnerabilidades costumam estar associadas a violações da segurança da memória, o código Rust deve reduzir significativamente o número de
CVEs críticas. Mas nem Rust é perfeito. Os desenvolvedores ainda precisam rastrear erros de correção e ataques de vazamento de dados. O suporte para bibliotecas seguras ainda requer análises de código, testes e difusão.
Compiladores não podem capturar todos os erros do programador. No entanto, o Rust retira nossa carga de segurança da memória, permitindo que nos concentremos na correção lógica do código.