Modelo simplificado do Fil-C
(corsix.org)- Estrutura que rastreia ponteiros C/C++ junto com metadados de AllocationRecord e realiza verificação de limites de memória na desreferenciação
- Método que move, junto com o valor original do ponteiro, os metadados correspondentes em atribuições de ponteiros, aritmética, passagem de argumentos de função, retorno e chamadas a
malloc·free, ou os converte em chamadas específicas do Fil-C - Os metadados de ponteiros dentro da memória heap são armazenados separadamente em invisible_bytes; ao carregar/armazenar ponteiros, o valor e os metadados são lidos e gravados juntos, com verificação de alinhamento adicional
filc_freelibera apenasvisible_byteseinvisible_bytes, mantendo o próprio AllocationRecord; a limpeza posterior fica a cargo do coletor de lixo, e variáveis locais cujo endereço possa escapar passam por promoção para heap- Embora ainda existam complexidades de implementação real, como threads, ponteiros de função e otimizações de memória/desempenho, há potencial de uso para verificação de segurança de memória em grandes bases C/C++ ou como exemplo concreto de sistema para pointer provenance
Modelo simplificado do Fil-C
- Fil-C usa uma estrutura que rastreia metadados de AllocationRecord* junto com ponteiros para tratar código C/C++ com segurança de memória
- A implementação real reescreve LLVM IR, mas o modelo simplificado transforma automaticamente o código-fonte C/C++
- Para cada variável local do tipo ponteiro em uma função, é adicionada uma variável local correspondente do tipo
AllocationRecord* - Por exemplo, para
T1* p1, é adicionadaAllocationRecord* p1ar = NULL
- Atribuições simples e cálculos sobre variáveis locais de ponteiro movem também o AllocationRecord* junto com o valor original do ponteiro
p1 = p2é transformado emp1 = p2, p1ar = p2arp1 = p2 + 10também vem acompanhado dep1ar = p2ar- Cast de inteiro para ponteiro define os metadados como
NULL - Cast de ponteiro para inteiro permanece inalterado
- Na passagem de argumentos e retorno de funções, também se passa um AllocationRecord* adicional junto com o ponteiro, e certas chamadas da biblioteca padrão são substituídas por funções específicas do Fil-C
- Chamadas a
mallocefreesão convertidas parafilc_mallocefilc_free - Por exemplo,
p1 = malloc(x); free(p1);vira{p1, p1ar} = filc_malloc(x); filc_free(p1, p1ar);
- Chamadas a
filc_mallocnão faz apenas uma alocação para a memória solicitada, mas três alocações- Alocação do objeto
AllocationRecord - Alocação de
visible_bytespara os dados reais - Alocação com
callocdeinvisible_bytespara armazenar metadados invisíveis AllocationRecordinclui os camposvisible_bytes,invisible_byteselength
- Alocação do objeto
Desreferenciação e verificação de limites
- Ao desreferenciar um ponteiro, usa-se o AllocationRecord* associado para realizar verificação de limites
- Verifica-se se os metadados do ponteiro não são
NULL - Calcula-se a diferença entre a posição atual do ponteiro e o endereço inicial de
visible_bytes - Verifica-se se o offset é menor que o comprimento total
- Verifica-se se o comprimento restante é suficiente para o tamanho do alvo da desreferenciação
- Verifica-se se os metadados do ponteiro não são
- O mesmo procedimento de verificação se aplica tanto a leitura quanto a escrita
- A verificação é feita antes de
x = *p1 - A mesma forma de verificação é feita antes de
*p2 = x
- A verificação é feita antes de
- Com essa estrutura, acessos fora da faixa alocada do alvo apontado pelo ponteiro são bloqueados
Ponteiros na heap e invisible_bytes
- Como o compilador não pode gerenciar diretamente ponteiros armazenados na memória heap com variáveis separadas, como faz com variáveis locais, usa-se invisible_bytes
- Se houver um ponteiro na posição
visible_bytes + i, oAllocationRecord*correspondente é armazenado na posiçãoinvisible_bytes + i - Em outras palavras,
invisible_bytesfunciona como um array cujo tipo de elemento éAllocationRecord*
- Se houver um ponteiro na posição
- Ao ler ou escrever um valor de ponteiro na memória, além da verificação de limites normal, adiciona-se verificação de alinhamento
- Verifica-se se o offset
ié múltiplo desizeof(AllocationRecord*) - Só com essa condição é possível acessar
invisible_bytescom segurança como um array deAllocationRecord**
- Verifica-se se o offset
- Ao carregar um ponteiro, os metadados também são carregados junto com o ponteiro de dados
p2 = *p1recebe, apósp2 = *p1, a adição dep2ar = *(AllocationRecord**)(p1ar->invisible_bytes + i)
- Ao armazenar um ponteiro, grava-se não só o valor do ponteiro, mas também os metadados correspondentes
*p1 = p2executa, após armazenar o dado real,*(AllocationRecord**)(p1ar->invisible_bytes + i) = p2ar
filc_free e o coletor de lixo
filc_freelibera duas regiões de memória após verificar a consistência com o AllocationRecord, quando o ponteiro não éNULL- Verifica
par != NULL - Verifica
p == par->visible_bytes - Libera
visible_byteseinvisible_bytes - Depois, define
visible_byteseinvisible_bytescomoNULLelengthcomo 0
- Verifica
- Embora
filc_mallocfaça três alocações,filc_freenão libera o próprio objeto AllocationRecord- Essa diferença é tratada pelo coletor de lixo
- No modelo simplificado, um GC stop-the-world já é suficiente; o Fil-C real usa um coletor paralelo, concorrente e incremental
- O GC rastreia seguindo os objetos
AllocationRecord AllocationRecordinacessíveis são marcados para liberação
- O GC rastreia seguindo os objetos
- O GC também executa mais duas tarefas
- Ao liberar um
AllocationRecordinacessível, chamafilc_free - Todos os ponteiros que apontam para um
AllocationRecordcomlengthigual a 0 são alterados para um únicoAllocationRecordcanônico de comprimento 0
- Ao liberar um
- Com esse comportamento, mesmo sem chamar
free, não há vazamento permanente de memória- O GC libera automaticamente
- Ainda assim, chamar
freepermite liberar memória mais cedo do que esperar pelo GC
- Depois de
free, oAllocationRecordcorrespondente acaba se tornando inacessível e pode ser limpo mais tarde
Escape de endereço de variável local e promoção para heap
- Com a existência de GC, amplia-se a faixa em que endereços de variáveis locais podem ser tratados com segurança
- Se o endereço de uma variável local foi obtido e o compilador não consegue provar que esse endereço não escapa para além da vida útil da variável, ela é promovida para alocação na heap
- Essas variáveis locais passam a ser alocadas com
mallocem vez de na pilha- Não é necessário inserir um
freecorrespondente - O GC fica responsável pela coleta
- Não é necessário inserir um
Versão Fil-C de memmove
- A
memmoveda biblioteca padrão C lida com memória arbitrária, o que cria o problema de o compilador não saber se há ponteiros nela - Para isso, aplica-se uma heurística
- Ponteiros dentro de memória arbitrária precisam estar totalmente contidos dentro daquele intervalo de memória
- Os ponteiros precisam estar corretamente alinhados
- Por essa regra, há diferença de comportamento mesmo ao mover os mesmos 8 bytes
- Se
memmovemover 8 bytes alinhados de uma vez, a faixa correspondente deinvisible_bytestambém é movida - Se
memmovemover esses 8 bytes em 8 operações de 1 byte,invisible_bytesnão é movido
- Se
Complexidades adicionais na implementação real
-
Threads
- Concorrência é um fator que aumenta a complexidade do GC
filc_freenão pode liberar memória imediatamente- Isso porque pode haver condição de corrida entre a thread que está liberando e outra thread acessando a mesma memória
- Operações atômicas sobre ponteiros também exigem tratamento adicional
- A reescrita básica transforma loads/stores de ponteiro em dois loads/stores, quebrando a atomicidade
-
Ponteiros de função
- Metadados adicionais em
AllocationRecordindicam sevisible_bytesnão são dados comuns, mas um ponteiro para código executável - Uma chamada por meio do ponteiro de função
p1verificap1 == p1ar->visible_bytese também sep1arrepresenta um ponteiro de função - Para evitar ataques de confusão de tipo com ponteiros de função, também é necessária validação de assinatura de tipo na ABI de chamada
- Um método é fazer com que todas as funções tenham a mesma assinatura de tipo
- Como se todos os argumentos fossem colocados em uma struct e passados via memória
- Na fronteira da ABI, cada função receberia apenas um único
AllocationRecordcorrespondente a essa struct
- Metadados adicionais em
-
Otimização de uso de memória
- É possível considerar fazer
filc_mallocnão alocarinvisible_bytesimediatamente, deixando isso para quando necessário - Também se pode considerar colocar
AllocationRecordevisible_bytesjuntos em uma única alocação - Se o
mallocsubjacente anexar metadados no início de cada alocação, também se pode considerar incorporar esses metadados aoAllocationRecord
- É possível considerar fazer
-
Otimização de desempenho
- A segurança de memória do Fil-C vem com custo de desempenho
- Há espaço para aplicar várias técnicas para recuperar parte do desempenho perdido
Quando usar Fil-C
- Pode ser usado quando uma grande base de código C/C++ parece funcionar, mas não tem verificação de segurança de memória, e há disposição para aceitar GC e grande perda de desempenho em troca dessa segurança
- Menciona-se a possibilidade de servir como medida temporária antes de reescrever em Java, Go ou Rust
- Também é possível executar Fil-C com o objetivo de detectar bugs de memória, como com ASan
- É possível rodar código C/C++ sob Fil-C para verificar bugs de memória
- Em linguagens em que a linguagem de compilação e de runtime é a mesma e a segurança em tempo de compilação é forte, pode haver uso para avaliação segura em tempo de compilação
- Zig é citado como exemplo
- Mesmo que a avaliação em runtime não seja segura, a avaliação em tempo de compilação pode usar a configuração do Fil-C
- Também tem valor como um exemplo concreto de sistema que lida com pointer provenance
- Levanta-se a questão de se, quando
p1ep2têm o mesmo tipo, a otimizaçãoif (p1 == p2) { f(p1); }→if (p1 == p2) { f(p2); }seria possível - No Fil-C, como o
AllocationRecord*passado parafpode ser diferente, a resposta é explicitamente não - Nesse ponto, o Fil-C serve como exemplo concreto de sistema com pointer provenance
- Levanta-se a questão de se, quando
1 comentários
Comentários no Hacker News
Também haveria espaço para testar contagem de referências ou variações do invisible capability system, e talvez desse até para economizar memória em troca de um pequeno custo de indireção
Espero que isso ajude quem quiser usar os dois juntos para fazer hermetic builds
Upon freeing an unreachable AllocationRecord, call filc_free on it.Pelo que entendi, a intenção era dizer que, antes de liberar um AR inalcançável, é preciso liberar primeiro a memória apontada pelos campos
visible_byteseinvisible_bytesMais do que dizer “rewrite it in Rust” por segurança, acho mais interessante o fato de ele conseguir compilar programas C existentes de forma totalmente memory-safe
Primeiro, Fil-C é mais lento e maior. Se isso fosse aceitável, daria até para dizer que, nos últimos 10 anos, teria feito mais sentido migrar antes para Java ou C# do que para Rust
Segundo, ainda continua sendo C. Para manter código existente, tudo bem, mas se for escrever muito código novo, eu acho Rust muito mais agradável
Terceiro, Fil-C oferece segurança em runtime, enquanto Rust consegue expressar parte disso em compile time. Indo além, linguagens como WUFFS tentam provar a segurança já na compilação sem checks em runtime, então o código pode até estar logicamente errado, mas a proposta é impedir crashes ou execução arbitrária de código
Houve threads como Fil-Qt: A Qt Base build with Fil-C experience, Linux Sandboxes and Fil-C, Ported freetype, fontconfig, harfbuzz, and graphite to Fil-C, A Note on Fil-C, Notes by djb on using Fil-C, Fil-C: A memory-safe C implementation e Fil's Unbelievable Garbage Collector
Ainda dá para escrever código que não é memory-safe, e agora isso significa mais que o resultado vira um crash garantido em vez de uma vulnerabilidade
Se você estiver criando algo como uma web API que recebe entrada não confiável, esse tipo de problema no fim pode virar denial-of-service, então é melhor, mas ainda está longe de ser bom o bastante
Não estou tentando diminuir o trabalho do Fil-C; só acho que abordagens de runtime têm limitações claras
Mas, para ser justo, Fil-C é bem mais lento que Rust e também usa mais memória
Por outro lado, Fil-C suporta safe dynamic linking e, em alguns aspectos, dá até para dizer que ele é mais estritamente seguro que Rust
No fim, são trade-offs, então acho que cada um deve escolher de acordo com o seu contexto
Por isso, mesmo que a ideia seja tecnicamente interessante, parece difícil que ela conquiste as pessoas no plano emocional
Os valores de capability e pointer podem ser corrompidos durante uma atribuição, então, se o interleaving entre threads for ruim, pode acabar acessando um objeto com um ponteiro incorreto e causar mau funcionamento arbitrário
Acho que essa limitação em si é aceitável, mas é decepcionante ver um clima em que até apoiadores atacam com força quem aponta esse problema
Infelizmente, isso também é uma das grandes causas do overhead
Esse tipo de abordagem já foi implementado e descartado muitas vezes, por oferecer garantias de segurança insuficientes, por ser difícil atravessar non-fat ABI boundaries ou por ter overhead alto
Além disso, eu diria que filc não se explica apenas como um simples fat pointer