4 pontos por GN⁺ 2023-08-31 | 1 comentários | Compartilhar no WhatsApp
  • 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 de Context
  • Context é uma struct que contém funcionalidades e estados como logs, PID, framebuffer, calloc, cdalloc, store e 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 return e, depois disso, a função start é 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 de Context
  • 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.sh e pode exigir rust nightly, gcc e qemu com flags de Virgl e SDL

1 comentários

 
GN⁺ 2023-08-31
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 para
    Do 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

    • Está certo, e acho que a contestação é quase uma mudança de foco do ponto principal
      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
    • Não dá para ver isso como tudo ou nada
      É possível permitir que apps sejam escalonados de forma cooperativa e, ainda assim, impedir que while(true){} prenda o sistema indefinidamente
      Por 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
    • Sou o autor
      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
    • Não sou desenvolvedor de sistemas operacionais, mas acho que o número de núcleos faz diferença
      Um loop while(true) derruba um sistema de núcleo único, mas não necessariamente um sistema multicore
      Os 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
    • Há um motivo para o mundo ter ido para a multitarefa preemptiva depois do experimento de multitarefa cooperativa do Windows 3.1
  • 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

    • Em Smalltalk, isso corresponde mais a métodos de classes do que a funções independentes, mas está certo no sentido de que todos os objetos dentro da imagem enviam mensagens uns aos outros, ou seja, chamam métodos uns dos outros
      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
    • Dizer que “apps são apenas funções” soa como a maldição do desenvolvimento greenfield
      Parece que os projetistas ainda não descobriram por que outros sistemas operacionais passaram a precisar das coisas que eles omitiram agora
    • Na prática, fico curioso sobre qual é a diferença em relação a outros sistemas operacionais nos quais um app é uma função int main() { … }
    • Isso parece bater com a definição de unikernel
      Outro exemplo feito em Rust é https://github.com/hermit-os/hermit-rs
    • Basta rodar o velho Classic MacOS
  • 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

    • Sou o autor
      É 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_uring seriam excelentes, mas este modelo parece excluir esse tipo de coisa, então pode ser difícil obter desempenho adequado
    També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

    • Suponho que E/S assíncrona seria implementada atualizando uma solicitação de E/S no Context e retornando; quando ela estivesse pronta, a função seria chamada de novo
      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

    • No mundo dos navegadores, todos os sites abertos compartilham a memória heap do navegador, mas não interferem uns nos outros
      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?
    • Esse exemplo é ainda mais forçado, porque assume que todos os sistemas têm um sandboxing péssimo como Windows e Linux
      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 execve
    Mesmo sem um executável, é possível criar processos com clone3 ou fork; 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ável
    Um 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

    • Pensando um pouco mais, acho que um dos grandes erros é a homogeneização dos formatos de executáveis
      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
    • Eu achava que o ponto forte do Linux eram os drivers, não uma ABI estável
      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
    • Fico curioso se você viu como o Zircon(Fuchsia) lida com isso
      É 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

    • Lembro que, em meados dos anos 2000, havia na Microsoft Research um protótipo de sistema operacional escrito apenas em .NET
      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 yield nos pontos necessários e impor multitarefa cooperativa de forma parecida
      O 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
    • Em sistemas Smalltalk como Squeak ou Pharo, quando uma thread trava, o usuário interrompe a execução com um atalho de teclado
      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?

    • Theseus é um exemplo da segunda abordagem implementada em Rust sem bytecode
      Pelo que entendi, regras como proibir unsafe sã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/
    • É especulação, mas talvez todos os “programas” compartilhem um único espaço de endereçamento, enquanto a memória virtual limita a visibilidade das páginas acessíveis em determinado momento
      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
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • 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

    • Acrescentando, ao dizer que “multitarefa cooperativa não significa necessariamente desempenho ruim”, eu estava me referindo a desempenho interativo ruim
      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/