Entrada em user mode e retorno controlado a ring 0¶
Escopo¶
Executar aplicação em CPL 3 exige mais do que saltar para um endereço lower-half. A CPU precisa entrar com seletores de usuário, stack de usuário válida, page tables cujos mapeamentos tenham permissão User/Supervisor adequada, um caminho de interrupção/syscall para voltar ao kernel e uma continuação de ring 0 controlada pelo kernel para exit/fault.
O ChrisOS faz a queda de privilégio via iretq em enter_user. Processos e syscalls ao redor dessa função definem CR3, mapeamentos, gate 0x80 DPL 3, validação de memória e a capacidade de reescrever o frame salvo quando o usuário termina ou falha.
CPL, seletores e permissões de página¶
Proteção x86-64 combina mecanismos. Os bits inferiores de CS definem Current Privilege Level. O descritor selecionado na GDT possui Descriptor Privilege Level. Page tables têm bit U/S para permitir ou proibir acesso de user mode.
Logo, contexto user exige simultaneamente: - seletores válidos para ring 3; - páginas marcadas como acessíveis ao usuário.
O ChrisOS define GDT_USER_DATA = 0x18 e GDT_USER_CODE = 0x20. enter_user combina ambos com 3 para RPL 3.
Isso não muda permissões de page table. proc_map_user acrescenta MM_USER aos mapeamentos destinados ao processo.
Contexto de address space¶
Cada processo possui CR3. proc_create chama mm_clone_kernel_space e cria a primeira página de stack de usuário.
proc_switch exige BSP; uma chamada em AP causa panic. Para PID válido, atualiza g_current e chama mm_switch(cr3).
Portanto enter_user pressupõe que address space correto já foi selecionado. A função não escolhe CR3. Essa separação mantém ownership de memória em proc/mm e deixa a primitiva de transição arquitetural pequena.
Regiões virtuais atuais¶
proc.h define bases:
| Região | Base |
|---|---|
| VM | 0x02000000 |
| heap | 0x04000000 |
| framebuffer | 0x06000000 |
| stack | 0x07F00000 |
| library | 0x08000000 |
Esses valores formam o layout usado pela implementação atual, não um ABI genérico completo.
A região de stack pode crescer por demand commit em quatro páginas a partir de PROC_STACK_VIRT. O RSP passado para enter_user deve terminar em uma página user gravável, pois operações de stack começam imediatamente.
Continuação do kernel¶
Antes de mudar CPL, enter_user obtém __builtin_return_address(0) e chama syscall_set_kernel_return.
Assim fica registrado um RIP de ring 0 associado ao call site. Exit e fault usam essa continuação para voltar ao kernel.
O estado é global, não um objeto por thread. Isso funciona dentro do modelo em que execução de usuário é serializada no BSP. Múltiplas tarefas user simultâneas em CPUs diferentes exigiriam estado de retorno per-task/per-CPU.
Frame de iretq¶
A função monta:
0x202 deixa IF ativo em user mode.
A asm empilha:
iretq restaura RIP, CS e flags; como há transição de CPL 0→3, também instala RSP/SS de usuário.
O hardware valida privilégios e descritores. Não é equivalente a escrever CS diretamente nem simplesmente “rodar código em endereço baixo”.
Validações feitas pela CPU¶
CS precisa apontar para descritor presente/executável compatível com CPL 3. SS precisa ser segmento de dados válido e gravável. RIP/RSP devem ser canônicos. Ao buscar a primeira instrução, page tables precisam permitir User access.
Inconsistências geram exceções como #GP ou #SS.
Mesmo depois de entrar, páginas legais mas ainda não residentes podem gerar #PF; o kernel pode materializá-las por demand paging e repetir a instrução.
Entrada no kernel via syscall¶
syscall_init altera o gate 0x80 para DPL 3. Aplicação executa int 0x80, a CPU atravessa para seletor de código de ring 0 e o caminho comum produz irq_frame.
A infraestrutura TSS/GDT fornece stack privilegiada confiável quando há mudança de ring. Por isso user mode depende de GDT/TSS mesmo que o isolamento principal seja paging.
irq_dispatch reconhece 0x80 e chama syscall_dispatch.
Restrição ao BSP¶
syscall_dispatch rejeita execução em CPU diferente de zero, colocando -1 em RAX e avançando RIP.
Isso acompanha a regra de proc_switch. O ChrisOS atual não migra contexto user para APs.
A restrição simplifica g_current e g_user_kernel_rip globais, mas limita escalabilidade.
Retorno normal de syscall¶
A maioria das syscalls grava resultado em RAX salvo, avança RIP em 2 bytes e retorna ao stub. O stub restaura estado e executa iretq para ring 3.
O incremento de 2 corresponde ao tamanho de int imm8 e ao convention adotado pelo frame do sistema. Essa convenção precisa permanecer consistente com o código user e os stubs.
Mudar para SYSCALL/SYSRET exigiria outra semântica de entry/return e registradores.
Exit e retorno a ring 0¶
Para SYS_EXIT, o kernel não quer executar a próxima instrução user. syscall_return_to_kernel altera o frame salvo:
- RIP = g_user_kernel_rip;
- CS = GDT_KERNEL_CODE;
- RFLAGS = 0x202;
- g_user_exited = 1.
Quando o stub executa iretq, o destino é ring 0.
Alterações futuras no layout de frame ou stack precisam revalidar essa transformação; retornar entre níveis de privilégio não é apenas substituir um pointer.
Fault de usuário¶
panic_user_fault, apesar do nome, não derruba o kernel. Registra fault do processo, destrói o PID, grava diagnóstico, coloca exit code -11 e usa o mesmo mecanismo de retorno ao kernel.
O dispatcher só escolhe esse caminho quando CS salvo indica origem user. Fault de ring 0 segue para panic global.
Assim uma falha de aplicação é contida no processo quando possível.
Demand paging¶
No #PF, antes de matar o processo, proc_fault_demand recebe CR2. Ele reconhece VM, stack, heap e framebuffer dentro dos limites atuais.
proc_commit aloca frame físico, zera, mapeia com flags de user/write, registra ownership e pode flushar TLB do processo atual.
Se isso resolver a causa, irq_dispatch retorna sem alterar RIP. A instrução de usuário é repetida.
Logo, enter_user não precisa materializar antecipadamente todas as páginas de regiões lazy. Precisa apenas do conjunto inicial capaz de iniciar execução, incluindo entry e stack.
Segurança¶
A fronteira resulta da composição:
- CS/SS de usuário têm RPL 3 e apontam a descritores DPL 3;
- mapeamentos user recebem MM_USER explicitamente;
- somente interfaces intencionais da IDT, como 0x80, têm DPL 3;
- operações privilegiadas permanecem inacessíveis ao CPL 3;
- ponteiros recebidos por syscall passam por tradução/checagem de flags, não apenas teste de canonicalidade.
Nenhuma dessas camadas sozinha é “todo o sandbox”.
Estado global¶
g_user_kernel_rip, exit flags e g_current são globais. A regra BSP-only torna isso coerente na revisão atual.
Para userspace SMP, esses campos precisariam migrar para estruturas por thread/processo/CPU e teardown teria que sincronizar com execução remota.
IF e interrupções¶
User RFLAGS inicial é 0x202, portanto IF=1. IRQs mascaráveis podem interromper aplicação quando controladores também permitem.
Interrupt gate limpa IF na entrada privilegiada; iretq restaura flags salvas no retorno. O kernel não precisa setar IF manualmente apenas para preservar o estado user.
NMI permanece independente de IF.
Segurança da stack¶
Stack user não pode funcionar como stack privilegiada confiável. A transição para ring 0 utiliza estado do TSS.
Na direção oposta, enter_user instala deliberadamente o RSP fornecido. O caller precisa apontá-lo para região válida do processo. Hardware detecta mapeamentos/seletores ilegais, mas a semântica do layout é responsabilidade do loader/runtime.
O TSS atual tem rsp0 fixo, não stack de kernel por user thread. Isso acompanha o modelo serial no BSP.
Falhas possíveis¶
Transição pode falhar por seletor inválido, endereço não canônico, página ausente sem demand handling, página supervisor-only, stack descriptor errado ou corrupção de GDT/TSS.
Depois de entrar, privileged instruction, ponteiro ilegal ou acesso fora das regiões podem faultar. Demand fault válido é recuperado; fault user inválido encerra o processo; fault kernel paralisa o sistema.
Corrupção de g_user_kernel_rip faria exit voltar ao endereço privilegiado errado. Apesar de pequeno, esse estado faz parte da integridade de control flow.
Desempenho¶
iretq + int 0x80 priorizam clareza e integração com o dispatcher comum, mas não são a interface de syscall de menor overhead disponível em x86-64. SYSCALL/SYSRET é alternativa futura.
Demand paging reduz commitment inicial e desloca custo para first touch, introduzindo latência de page fault.
A decisão sobre otimização deve ser baseada em medição, não apenas no custo teórico da instrução.
Validação¶
Gates relevantes devem provar: - CPL 3 depois da entrada; - todas as syscalls suportadas; - IRQ durante execução user; - demand page com retry transparente; - acesso inválido terminando somente o processo; - exit retornando à continuação kernel correta; - páginas supervisor-only inacessíveis; - política BSP-only preservada.
Fonte mostra mecanismo; testes demonstram que descritores, paging e frame realmente concordam.
Limitações atuais¶
Execução user é BSP-only. Return target é global. TSS usa stack ring-0 fixa. Syscall é int 0x80. Layout virtual e número de processos/páginas são limitados por constantes.
São limites do ChrisOS atual, não limites inerentes ao x86-64.
Mapa de fonte¶
A transição está em kernel/metal/user_enter.c. GDT/TSS em gdt.c/gdt.h. CR3 e regiões user em proc.c/proc.h. Retorno e syscall 0x80 em syscall.c/syscall.h; idt.c expõe DPL 3. O Source Atlas publica todos integralmente na revisão da3df29cb397932c43d32373871fb9380e688ade.