Olá Habr! Apresento a você a tradução do artigo "O que é inseguro da Rust?" autor Nora Codes.
Eu já vi muitos mal-entendidos sobre o que a palavra-chave insegura significa para a utilidade e correção da linguagem Rust e sua promoção como uma "linguagem de programação de sistema segura". A verdade é muito mais complicada do que pode ser descrita em um pequeno tweet, infelizmente. É assim que eu a vejo.
Em geral, a palavra-chave não segura não desativa o sistema de tipos que mantém o código de ferrugem correto . Isso só permite o uso de algumas "superpotências", como ponteiros de referência. inseguro é usado para implementar abstrações seguras com base em um mundo fundamentalmente inseguro, para que a maioria dos códigos Rust possa usar essas abstrações e evitar acesso inseguro à memória.
Garantia de segurança
A ferrugem garante a segurança como um dos seus princípios fundamentais. Podemos dizer que esse é o significado da existência da linguagem. No entanto, ele não fornece segurança no sentido tradicional, durante a execução do programa e usando o coletor de lixo. Em vez disso, o Rust usa um sistema de tipo muito avançado para acompanhar quando e quais valores podem ser acessados. O compilador analisa estaticamente cada programa Rust para garantir que ele esteja sempre no estado correto.
Segurança Python
Vamos usar o Python como exemplo. Código Python puro não pode corromper a memória. O acesso aos itens da lista tem verificações para ir além das fronteiras; os links retornados pelas funções são contados para evitar o aparecimento de links pendentes; Não há como fazer aritmética arbitrária com ponteiros.
Isso tem duas consequências. Primeiro, muitos tipos devem ser "especiais". Por exemplo, não é possível implementar uma lista ou dicionário eficaz em Python puro. Em vez disso, o intérprete CPython tem sua implementação interna. Em segundo lugar, o acesso a funções externas (funções não implementadas no Python), chamado de interface de uma função externa, requer o uso de um módulo ctypes especial e viola as garantias de segurança da linguagem.
De certa forma, isso significa que tudo escrito em Python não garante acesso seguro à memória.
Segurança em Ferrugem
O Rust também fornece segurança, mas, em vez de implementar estruturas não seguras em C, fornece um truque: a palavra-chave não segura. Isso significa que as estruturas de dados fundamentais no Rust, como Vec, VecDeque, BTreeMap e String, são implementadas no Rust.
Você pode perguntar: "Mas, se o Rust fornecer um truque contra suas garantias de segurança de código, e a biblioteca padrão for implementada usando esse truque, tudo no Rust não será considerado perigoso?"
Em uma palavra, caro leitor, sim , exatamente do jeito que era em Python. Vamos dar uma olhada em mais detalhes.
O que é proibido na ferrugem segura?
A segurança no Rust está bem definida: pensamos muito sobre isso. Em resumo, os programas Rust seguros não podem:
- Desreferenciando um ponteiro que aponte para um tipo diferente do que o compilador conhece . Isso significa que não há ponteiros para nulo (porque eles não apontam para lugar nenhum), erros de ultrapassagem dos limites e / ou erros de segmentação (falhas de segmentação), nenhum estouro de buffer. Mas também significa que não há utilidade após liberar a memória ou liberar novamente a memória (porque liberar a memória é considerada desreferenciando o ponteiro) e nenhum trocadilho destinado à digitação .
- Ter várias referências mutáveis para um objeto ou simultaneamente referências mutáveis e imutáveis para um objeto . Ou seja, se você tiver uma referência mutável a um objeto, só poderá tê-lo e se tiver uma referência imutável ao objeto, ele não será alterado até que você o mantenha. Isso significa que você não pode forçar uma corrida de dados no Safe Rust, que é uma garantia que a maioria dos outros idiomas seguros não pode fornecer.
Rust codifica essas informações em um sistema de tipos ou usando tipos de dados algébricos , como Option para indicar a existência / ausência de um valor e Result <T, E> para indicar erro / sucesso ou referências e sua vida útil , por exemplo, & T vs & mut T para indicar um link comum (imutável) e um link exclusivo (mutável) e & 'a T vs &' b T para distinguir links que estão corretos em diferentes contextos (isso geralmente é omitido, pois o compilador é inteligente o suficiente para descobrir por si mesmo) .
Exemplos
Por exemplo, o código a seguir não será compilado, pois contém um link pendente. Mais especificamente, my_struct não vive o suficiente . Em outras palavras, a função retornará um link para algo que não existe mais e, portanto, o compilador não pode (e, de fato, nem sabe como) compilar isso.
fn dangling_reference(v: &u64) -> &MyStruct {
Esse código faz o mesmo, mas tenta solucionar esse problema colocando o valor no heap (Box é o nome do ponteiro inteligente de base no Rust).
fn dangling_heap_reference(v: &u64) -> &Box<MyStruct> { let my_struct = MyStruct { value: v };
O código correto é retornado pelo próprio Box em vez de uma referência a ele. Isso codifica a transferência de propriedade - a responsabilidade de liberar memória - na assinatura da função. Ao olhar para a assinatura, fica claro que o código de chamada é responsável pelo que acontece com o Box e, de fato, o compilador a processa automaticamente.
fn no_dangling_reference(v: &u64) -> Box<MyStruct> { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Algumas coisas ruins não são proibidas no Rust seguro. Por exemplo, é permitido do ponto de vista do compilador:
- causar impasse no programa
- vazar uma quantidade arbitrariamente grande de memória
- falha ao fechar alças de arquivo, conexões de banco de dados ou tampas de eixo de mísseis
A força do ecossistema Rust é que muitos projetos optam por usar um sistema de tipos para garantir que o código seja o mais preciso possível, mas o compilador não exige essa coerção, exceto nos casos em que o acesso seguro à memória é fornecido.
O que é permitido no Rust inseguro?
Código de ferrugem inseguro é o código de ferrugem com a palavra-chave insegura. inseguro pode ser aplicado a uma função ou bloco de código. Quando aplicada a uma função, significa "essa função requer que o código chamado forneça manualmente a invariante geralmente fornecida pelo compilador". Quando aplicado a um bloco de código, significa "esse bloco de código fornece manualmente o invariante necessário para impedir o acesso inseguro à memória e, portanto, é permitido fazer coisas inseguras".
Em outras palavras, inseguro para a função significa "você precisa verificar tudo" e, no bloco de código - "Eu já verifiquei tudo".
Conforme observado em The Rust Programming Language , o código em um bloco marcado com a palavra-chave não segura pode:
- Desreferenciar um ponteiro. Essa é uma "superpotência" importante que permite implementar listas duplamente vinculadas, mapa de hash e outras estruturas de dados fundamentais.
- Chame uma função ou método não seguro. Mais sobre isso abaixo.
- Acesse ou modifique uma variável estática mutável. Variáveis estáticas cujo escopo não é controlado não podem ser verificadas estaticamente; portanto, seu uso é inseguro.
- Implementar característica insegura. Características inseguras são usadas para sinalizar se tipos específicos garantem certos invariantes. Por exemplo, Enviar e sincronizar determinam se um tipo pode ser enviado entre os limites do encadeamento ou pode ser usado por vários encadeamentos simultaneamente.
Lembra daqueles ponteiros pendurados acima? Adicione a palavra inseguro, e o compilador jurará o dobro, porque ele não gosta de usar inseguros onde não é necessário.
Em vez disso, a palavra-chave não segura é usada para implementar abstrações seguras com base em operações arbitrárias de ponteiros. Por exemplo, o tipo Vec é implementado sem segurança, mas é seguro usá-lo, pois verifica as tentativas de acessar elementos e não permite estouros. Embora ofereça operações como set_len, que podem causar acesso inseguro à memória, elas são marcadas como inseguras.
Por exemplo, poderíamos fazer o mesmo que no exemplo no_dangling_reference, mas com um uso irracional de inseguro:
fn manual_heap_reference(v: u64) -> *mut MyStruct { let my_struct = MyStruct { value: v }; let my_box = Box::new(my_struct);
Observe a falta da palavra insegura. Criar ponteiros é absolutamente seguro. Como foi escrito, esse é um risco de vazamento de memória, mas nada mais, e os vazamentos de memória são seguros. Chamar esta função também é seguro. inseguro é necessário apenas quando algo tenta desreferenciar um ponteiro. Como um bônus adicional, a desreferenciação liberará automaticamente a memória alocada.
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct = unsafe { Box::from_raw(my_pointer) };
Após a otimização, esse código é equivalente a simplesmente retornar uma caixa. Box é uma abstração segura baseada em ponteiro, porque impede a distribuição de ponteiros em todos os lugares. Por exemplo, a próxima versão do main levará a uma memória livre dupla (livre dupla).
fn main() { let my_pointer = manual_heap_reference(1337); let my_boxed_struct_1 = unsafe { Box::from_raw(my_pointer) };
Então, o que é abstração segura?
Abstração segura é uma abstração que usa um sistema de tipos para fornecer uma API que não pode ser usada para violar as garantias de segurança mencionadas acima. A caixa é mais segura * mut T, pois não pode levar à dupla desalocação de memória, como ilustrado acima.
Outro exemplo é o tipo Rc em Rust. Este é um ponteiro de contagem de referência - uma referência não alterável aos dados no heap. Como permite o acesso múltiplo simultâneo a uma área da memória, ele deve impedir a alteração para ser considerado seguro.
Além disso, não é seguro para threads. Se você precisar de segurança de encadeamento, precisará usar o tipo de arco (contagem de referência atômica), que tem uma penalidade de desempenho devido ao uso de valores atômicos para a contagem de links e para evitar possíveis corridas de dados em ambientes com vários encadeamentos.
O compilador não permitirá que você use Rc onde você deve usar o Arc, porque criadores como Rc não o marcaram como seguro para threads. Se eles fizessem isso, seria irracional: uma promessa falsa de segurança.
Quando é necessário Rust inseguro?
Ferrugem insegura é sempre necessária quando é necessário executar uma operação que viole uma dessas duas regras descritas acima. Por exemplo, em uma lista duplamente vinculada, a ausência de links mutáveis para os mesmos dados (para o próximo elemento e o elemento anterior) priva completamente os benefícios. Com segurança, um implementador de lista duplamente vinculado pode escrever código usando ponteiros * mut Node e encapsulá-lo em uma abstração segura.
Outro exemplo está trabalhando com sistemas embarcados. Geralmente, os microcontroladores usam um conjunto de registros cujos valores são determinados pelo estado físico do dispositivo. O mundo não pode parar enquanto você tira e muda esse registro, portanto, é inseguro para trabalhar com caixas de suporte de dispositivo. Normalmente, essas caixas encapsulam o estado em invólucros transparentes e seguros que copiam dados sempre que possível ou usam outras técnicas que fornecem garantias ao compilador.
Às vezes, é necessário realizar uma operação que possa levar a leitura e gravação simultânea ou acesso inseguro à memória, e é aqui que é necessário um risco. Mas, desde que haja uma oportunidade de garantir que os invariantes seguros sejam mantidos antes que um usuário toque em algo (isto é, inseguro), tudo estará bem.
De quem está essa responsabilidade?
Chegamos a uma declaração feita anteriormente - sim , a utilidade do código Rust é baseada em código não seguro. Apesar de isso ser feito de uma maneira um pouco diferente da implementação insegura de estruturas básicas de dados em Python, a implementação de Vec, Hashmap etc. deve usar manipulações de ponteiro até certo ponto.
Dizemos que o Rust é seguro, com a suposição fundamental de que o código não seguro que usamos através de nossas dependências na biblioteca padrão ou no código de outras bibliotecas é corretamente escrito e encapsulado. A vantagem fundamental do Rust é que o código não seguro é direcionado para blocos não seguros que devem ser cuidadosamente verificados por seus autores.
No Python, o ônus de verificar a segurança das manipulações de memória fica apenas com os desenvolvedores dos intérpretes e usuários das interfaces de funções externas. Em C, esse ônus recai sobre todo programador.
No Rust, está com os usuários da palavra-chave não segura. Isso é óbvio, pois os invariantes devem ser mantidos manualmente dentro desse código e, portanto, é necessário buscar a menor quantidade desse código na biblioteca ou no código do aplicativo. A insegurança é detectada, destacada e indicada. Portanto, se segfaults ocorrer no seu código Rust, você encontrará um erro no compilador ou em várias linhas do seu código não seguro.
Este não é um sistema perfeito, mas se você precisar de velocidade, segurança e multithreading ao mesmo tempo, essa é a única opção.