Fomos: sistema operacional experimental criado em Rust
(github.com/Ruddle)- Fomos é um sistema operacional experimental criado em Rust, um projeto para entender ideias de sistemas operacionais non-Unix e os desafios do padrão exo-kernel
- Oferece saída gráfica, alocação dinâmica, carregamento e execução simultâneos de apps, suporte a mouse e teclado Virtio e escalonamento cooperativo
- Os apps são tratados como uma única função no formato
pub extern "C" fn _start(ctx: &mut Context) -> i32, sem biblioteca padrão, recebendo funcionalidades do SO por meio deContext Contexté uma struct que contém funcionalidades e estados como logs, PID, framebuffer,calloc,cdalloc,storee entrada; a compatibilidade com apps antigos é mantida adicionando novas funcionalidades ao final- Não há chamadas de sistema; os apps devolvem o controle ao SO com
returne, depois disso, a funçãostarté chamada novamente, em um modelo de execução cooperativo - O estado dos apps pode ser salvo em
Context.store, e o loop do kernel tem uma estrutura simplificada que percorre a lista de apps e chama_start(Context::new(...))de cada um - Como todas as funcionalidades e efeitos colaterais são transmitidos por
Context, parte-se da premissa de que sandboxing, instrumentação e depuração podem ser configurados substituindo ou encapsulando as funções deContext - Atualmente, a segurança não foi implementada, e os apps conseguem inspecionar a RAM de outros apps; há um plano para implementar segurança de dados sem context switch nem pilhas de memória virtual por app
- Itens ausentes incluem armazenamento persistente, suporte a GPU, rede e abstrações para compartilhamento de dados e funcionalidades entre apps; o Virgl está em desenvolvimento
- A build é executada com
./build.she pode exigirrust nightly,gcceqemucom flags de Virgl e SDL
1 comentários
Opiniões no Hacker News
O que não me agrada é que, em um sistema preemptivo,
while (true)pode até deixar o sistema mais lento, mas, em um sistema cooperativo, no momento em que ele não devolve o controle, a máquina praticamente paraDo ponto de vista de segurança, esse tipo de sistema também torna ataques de negação de serviço fáceis demais, e um único bug em um app pode se espalhar para o sistema inteiro
Não sou desenvolvedor de sistemas operacionais, então gostaria que me corrigissem se eu estiver errado
O motivo pelo qual os sistemas operacionais abandonaram a multitarefa cooperativa não foi porque resolveram para sempre todos os problemas de “uso descontrolado de recursos”, mas porque erros simples no nível do app, como bloqueio da thread de UI ou loops infinitos acidentais, estragam o estado do sistema inteiro
Em um sistema projetado para executar programas arbitrários, isso é bem crítico
É possível permitir que apps sejam escalonados de forma cooperativa e, ainda assim, impedir que
while(true){}prenda o sistema indefinidamentePor exemplo, dá para impor um limite de tempo por app ou, de forma mais experimental, detectar loops e aplicar um limite de tempo generoso
Para programadores, é conveniente quando o isolamento entre programas não relacionados é máximo e o isolamento entre programas relacionados é mínimo, mas, para usuários, é conveniente quando recursos de computação são fortemente isolados e coisas como armazenamento ou permissões ficam menos bloqueadas
Na prática, é necessário um escalonamento complexo que seja mais próximo do preemptivo do que do cooperativo, mas ainda um pouco cooperativo
Mesmo no Linux, programas maliciosos como fork bombs não têm dificuldade em travar o sistema, especialmente se o swap estiver ativado; e, mesmo com um escalonador preemptivo, se um programa ocupar 99% das threads do sistema, na prática é esse programa que vai rodar na maior parte do tempo
Escalonamento é um espectro, e os sistemas operacionais atuais são preemptivos, mas também são cooperativos em certa medida
Um app pode decidir se vai ceder o controle
Por outro lado, dá para manter o sistema operacional cooperativo, mas, quando ocorrer um limite de uso de recursos ou uma interrupção de timer, raramente fazer uma troca de contexto em modo de falha, mudar para preemptivo, encerrar o app e depois voltar ao cooperativo
Dá para chamar isso de cooperativo otimista e preemptivo pessimista
Um loop
while(true)derruba um sistema de núcleo único, mas não necessariamente um sistema multicoreOs pontos de compromisso podem ter mudado entre a época em que a estrutura básica dos sistemas operacionais que usamos hoje foi criada e agora
Gostei especialmente da parte em que “no Fomos, apps são apenas funções”
Executáveis Unix ou Windows são muito complexos em comparação com funções independentes, então é difícil até imaginar o quão legal seria um kernel escrito desse jeito
Fico curioso se Smalltalk/Squeak também funciona assim, e espero que o autor continue avançando com sistema de arquivos, gerenciador de tarefas, pilha de memória segura e compartilhamento de recursos
Claro, como requisito mínimo de prova de conceito, também precisa rodar DOOM
Sistemas operacionais de máquinas Lisp são mais próximos: no início, funções independentes chamavam umas às outras sem um sistema de objetos; depois, viraram funções genéricas especializadas na classe dos argumentos
Parece que os projetistas ainda não descobriram por que outros sistemas operacionais passaram a precisar das coisas que eles omitiram agora
int main() { … }Outro exemplo feito em Rust é https://github.com/hermit-os/hermit-rs
A ideia é boa, mas continuar anexando novas funções à struct Context para manter compatibilidade com itens antigos é um caminho para o inferno da retrocompatibilidade
É como se prender a uma situação em que você não consegue remover itens antigos ou obsoletos da struct Context
Um jeito melhor parece ser introduzir versionamento semântico entre o sistema operacional e os apps
Se um app declarar para qual versão do sistema operacional foi compilado ou de qual depende, o sistema operacional pode verificar a compatibilidade e passar a versão correspondente da struct Context
A maior parte dos problemas de retrocompatibilidade permanece, mas dá para manter a struct Context limpa mantendo várias structs por versão principal/secundária dentro do kernel
É uma boa ideia, mas também gosto da simplicidade de haver apenas uma interface de runtime
Se, de todo modo, o sistema operacional precisa lidar com todas as versões, apps futuros poderiam usar padding para dar a sensação de algo “limpo”
struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }Dito isso, escrevendo assim, também parece meio errado
Declarar a própria versão do app parece um problema que formatos de executável como ELF já resolvem, então estou tentando uma alternativa
A parte “como fazer sleep ou esperar de forma assíncrona? basta retornar” é meio estranha
Entrada e saída assíncronas no estilo
io_uringseriam excelentes, mas este modelo parece excluir esse tipo de coisa, então pode ser difícil obter desempenho adequadoTambém é estranho não oferecer suporte a async, porque isso poderia se conectar a pontos naturais de suspensão
Mas, para fazer isso, parece que seria preciso abrir mão de boa parte do design em que o estado da aplicação é salvo e carregado explicitamente no disco, e o custo parece alto
Acho que networking também pode ficar difícil, pelo menos de forma eficiente, por motivos semelhantes
Essa função parece a ponta final de um loop de eventos que recebe um estado arbitrário como parâmetro, então o que um loop de eventos faz provavelmente pode ser generalizado em grande parte
No entanto, isso significa abrir mão de corrotinas e suporte a async no nível da linguagem
O exemplo apresentado é forçado demais
Em um sistema operacional preemptivo, um app normalmente trava de uma forma que não transforma tudo em cooperativo, como em um deadlock de threads ou um loop infinito
Além disso, um sistema preemptivo pode encerrar um app se ele criar threads ou arquivos demais, ou usar memória demais, muito antes de, na prática, virar cooperativo
Nosso sistema é apenas mais permissivo
Além disso, dizer que “o sandboxing sai de graça se você aceitar as premissas”, mas também que “qualquer app consegue verificar facilmente a RAM de outro app, e isso é um problema difícil de resolver”, significa que sandboxing não sai de graça
Ainda assim, é uma ideia bacana e torço para que o autor se dê bem
A solução para esse problema pode ser criar uma função, ou seja, uma closure que envolva a aplicação e atue como o Context da própria app
Fico pensando: e se um app pudesse abrir outro app, ou se um app pudesse ser o sistema operacional para outro app?
Um sistema projetado corretamente para sandboxing robusto imporia limites a todos os recursos e negaria solicitações quando o limite fosse atingido
Fico curioso sobre como o Fomos distingue processos de executáveis
No Linux, um processo é um dado interno do kernel, como uid e gid, além de um espaço de endereços virtual que inclui ponteiros argv/envp, pilha, heap, máscara de sinais, tabela de handles de arquivos, handlers de sinais e memória executável
Um executável é um arquivo que contém bits suficientes para que o loader preencha esse espaço de endereços durante a chamada de sistema
execveMesmo sem um executável, é possível criar processos com
clone3oufork; o kernel usa ELF e a maior parte do espaço de usuário usa o loader RTLD da GLIBC, mas nenhum dos dois é estritamente necessário para criar um processo a partir de um formato específico de executávelUm executável estaticamente linkado sem código independente de posição fica mais próximo de “apenas uma função” do ponto de vista do assembler, mas resolver símbolos em tempo de execução sem ASLR o torna vulnerável a ataques de buffer overflow quando os endereços das funções dependentes são conhecidos
Eu gostaria de alternativas às falhas da glibc e ao modelo de processos do Posix, mas vejo boa parte da complexidade dos executáveis Unix como algo essencial
A resolução de símbolos em tempo de execução é difícil, mas útil; permitir interpretadores arbitrários é incômodo, mas é uma área em que o Linux é mais forte que Windows e MacOS; e oferecer uma interface de kernel por meio de chamadas de sistema estáveis é um ponto forte do Linux
Mac usa Mach-O, Windows usa PE, Linux usa ELF, e assim por diante, mas não há motivo para não existir um ecossistema diverso de formatos de execução/linkagem
Um sistema operacional com um modelo muito simples de carregamento de código seria um bom lugar para esse tipo de experimento
Uma ABI estável nem é algo exclusivo do Linux, e a utilidade dessa decisão também me parece bastante questionável, mas o suporte a drivers é excelente e difícil de negar
É bem interessante
Não sei como seria possível alcançar algum grau de segurança e safety com apps cooperativos não confiáveis
Qualquer app pode monopolizar a CPU indefinidamente e travar o kernel e os outros apps
Usamos sistemas operacionais com escalonamento preemptivo porque eles permitem interromper apps com mau funcionamento sem arruinar o restante do sistema
Ele usava multitarefa preemptiva, mas não impunha proteção de memória; em vez disso, tratava o compilador como um serviço do sistema, permitindo executar apenas executáveis produzidos e assinados pelo compilador do sistema
Como o compilador garantia a proteção de memória no momento do build, chamadas de sistema e comunicação entre processos ficavam muito baratas
Um compilador um pouco mais sofisticado poderia inserir chamadas
yieldnos pontos necessários e impor multitarefa cooperativa de forma parecidaO problema geral da parada não pode ser resolvido, mas ainda existem classes de programas para as quais a análise estática consegue provar que eles terminam ou cedem o controle
Bastaria tratar separadamente os programas que não podem ser provados, e um watchdog timer também poderia interromper automaticamente programas com mau funcionamento
Código não confiável não é executado na imagem “principal”, mas em uma VM descartável
Aqui também daria para aplicar o mesmo modelo usando um hipervisor, mas ninguém usa apenas um sistema Smalltalk isoladamente; é necessária alguma infraestrutura
Fico curioso para saber se é possível implementar segurança neste sistema operacional sem um redesenho completo, ou seja, sem refazer na prática o que os sistemas operacionais existentes já fizeram
Conheço duas formas de impor segurança para aplicações executadas no mesmo hardware
Uma é isolar processos em tempo de execução com memória virtual, e a outra é o loader verificar, no momento do carregamento, se o código faz acessos arbitrários à memória
A segunda normalmente é imposta por uma máquina virtual que só permite bytecode de um conjunto restrito de instruções sem aritmética de ponteiros, como a JVM ou Smalltalk
O autor do Fomos não quer troca de contexto nem isolamento de memória, entre outras coisas, e o compilador Rust não gera bytecode; haveria outro caminho?
Pelo que entendi, regras como proibir
unsafesão impostas por um compilador certificado, então aqui o código-fonte na prática equivale ao bytecodeÀ primeira vista parece muito com o Midori, mas a implementação nos detalhes é bem diferente
No Theseus, drivers, aplicações etc. são objetos ELF, todos vinculados dinamicamente em um único executável, isto é, o kernel, e há técnicas interessantes como hot upgrade
https://github.com/theseus-os/Theseus
https://www.theseus-os.com/
Seria algo como, quando ocorre uma falha de segmentação em tempo de execução, verificar um token de segurança para saber se o chamador tem permissão para acessar e chamar aquela página
Não sei o quão prático isso seria na realidade
Mesmo sendo multitarefa cooperativa, acho que hoje não seria como na época do Classic MacOS, porque agora há muitos cores
Um ou dois processos que não cedem a vez talvez não travem necessariamente o sistema inteiro
Se uma função se comportar mal e não retornar, o sistema poderia encerrá-la quando ela consumisse todos os cores
Multitarefa cooperativa não significa necessariamente desempenho ruim
O compartilhamento de tempo originalmente era uma forma de dividir uma enorme CPU única entre vários usuários, mas agora que CPUs multicore de usuário único são comuns, já passou da hora de pensar em outras formas de usar os cores
O fato de este projeto existir é realmente animador
Nesse modelo não há troca de contexto, então há até a possibilidade de o desempenho melhorar
Por isso fico curioso para saber o que aconteceria se o timeslice do Linux fosse aumentado para um valor absurdo, como 10 segundos
Gostaria de ouvir o plano de segurança com mais detalhes
No geral, acho que experimentos assim mostram que sistemas operacionais podem ser melhorados com um projeto greenfield
Isso me lembra um pouco o Mirage OS: https://mirage.io/