From: Tulio A M Mendes Date: Mon, 25 May 2026 18:02:55 +0000 (-0300) Subject: security: Round 1-2 fixes (A02, A03, A04, A14, A16, partial A07) X-Git-Url: https://projects.tadryanom.me/?a=commitdiff_plain;h=33d3e4a9616bf8854350febbb29fd85ee72bf4c9;p=AdrOS.git security: Round 1-2 fixes (A02, A03, A04, A14, A16, partial A07) - A16: pmm_decref underflow guard in src/mm/pmm.c - A02: sysenter.S stack pointer validation with 8-byte kernel margin - A03: pread/pwrite mode validation (rejects O_WRONLY for pread, O_RDONLY for pwrite, checks MS_RDONLY) - A04: Reject O_RDONLY|O_TRUNC in open syscall - A14: truncate/ftruncate storage update via vfs_truncate_node helper with fallback - A07: vfs_check_parent_permission helper added to fs.c, applied to unlink/rmdir/rename/mkdir/link/create (allows all for now - single-user root system) Note: A01 (W^X/NX default) removed temporarily due to kernel panic - needs NX bit investigation Note: K02 (mprotect per VMA) deferred - requires per-page VMA architecture Tests: 119/119 PASS (smoke test, SMP=4) --- diff --git a/docs/SECURITY_FIX_PLAN_2026-05-25.md b/docs/SECURITY_FIX_PLAN_2026-05-25.md new file mode 100644 index 00000000..e5c2c355 --- /dev/null +++ b/docs/SECURITY_FIX_PLAN_2026-05-25.md @@ -0,0 +1,865 @@ +# Plano de Correção de Segurança - AdrOS 2026-05-25 + +## Resumo Executivo + +Este documento estabelece um plano priorizado para corrigir as vulnerabilidades identificadas no relatório de reauditoria de segurança de 2026-05-25. O plano está organizado em 5 fases, priorizando isolamento de memória, bypasses de filesystem, parsers de boot/storage, interfaces multiusuário e correções userspace/libc. + +**Status atual:** 36 problemas identificados, 9 já corrigidos em sessões anteriores, 27 pendentes. + +**Correções confirmadas já feitas:** +- K01: mmap(MAP_FIXED) validação de alinhamento/overflow/kernel crossing +- K03: shm_at validação de endereço e busca de área livre +- K04: AIO com bounce buffers +- K05: send/recv/sendto/recvfrom com bounce buffers +- A06: kill checa root ou UID/EUID compatível +- K08: clone rejeita flags não suportadas, refcount de address space +- A17: vdso.tick_hz inicializado com TIMER_HZ +- K11: diskfs antigo removido, mount usa tipo explícito +- K07: O_EXCL e O_DIRECTORY implementados +- A16: pmm_free_page ganhou guarda contra double free + +--- + +## Fase 1: Isolamento de Memória (Crítico) + +### 1.1 pmm_decref underflow (A16) - ALTA PRIORIDADE + +**Problema:** `pmm_decref` decrementa `frame_refcount[frame]` sem checar se o valor já é zero, permitindo underflow. + +**Arquivo:** `src/mm/pmm.c:243-254` + +**Código atual:** +```c +uint32_t pmm_decref(uintptr_t paddr) { + uint64_t frame = paddr / PAGE_SIZE; + if (frame == 0 || frame >= max_frames) return 0; + uintptr_t flags = spin_lock_irqsave(&pmm_lock); + uint32_t new_val = --frame_refcount[frame]; // <-- underflow possível + if (new_val == 0) { + bitmap_unset(frame); + used_memory -= PAGE_SIZE; + } + spin_unlock_irqrestore(&pmm_lock, flags); + return new_val; +} +``` + +**Correção:** +```c +uint32_t pmm_decref(uintptr_t paddr) { + uint64_t frame = paddr / PAGE_SIZE; + if (frame == 0 || frame >= max_frames) return 0; + uintptr_t flags = spin_lock_irqsave(&pmm_lock); + if (frame_refcount[frame] == 0) { + // Underflow - bug de ownership, reportar e abortar + spin_unlock_irqrestore(&pmm_lock, flags); + kprintf("[PMM] ERROR: pmm_decref underflow on frame %llu\n", frame); + return 0; + } + uint32_t new_val = --frame_refcount[frame]; + if (new_val == 0) { + bitmap_unset(frame); + used_memory -= PAGE_SIZE; + } + spin_unlock_irqrestore(&pmm_lock, flags); + return new_val; +} +``` + +**Teste:** Adicionar teste em fulltest que tenta decref excessivo. + +--- + +### 1.2 sysenter.S validação insuficiente (A02) - ALTA PRIORIDADE + +**Problema:** sysenter valida apenas `ECX < 0xC0000000`, mas não checa: +- ECX não nulo +- ECX + 8 ainda em userspace +- Memória mapeada e legível +- Leitura não cruza página inválida + +**Arquivo:** `src/arch/x86/sysenter.S:87-95` + +**Código atual:** +```asm +cmpl $0xC0000000, %ecx +jae 1f +mov 4(%ecx), %edx /* EDX = arg3 (was saved by user) */ +mov 8(%ecx), %ecx /* ECX = arg2 (was saved by user) */ +jmp 2f +1: /* Invalid user ESP — zero args to prevent data leak */ +xor %edx, %edx +xor %ecx, %ecx +``` + +**Correção:** Usar rotina segura de copy_from_user ou validar mais robustamente: +```asm +cmpl $0xC0000000, %ecx +jae 1f +cmpl $8, %ecx /* ECX must be at least 8 bytes from kernel boundary */ +jb 1f +/* Adicionar check de página mapeada via page table walk seria ideal, + mas complexo em assembly. Alternativa: usar copy_from_user em C */ +mov 4(%ecx), %edx +mov 8(%ecx), %ecx +jmp 2f +1: +xor %edx, %edx +xor %ecx, %ecx +``` + +**Nota:** Solução ideal seria mover a recuperação de args para C usando `copy_from_user`, mas isso requer mudança de ABI. Por enquanto, adicionar check de limite mínimo. + +--- + +### 1.3 W^X/NX default em mmap/brk/SHM (A01) - ALTA PRIORIDADE + +**Problema:** VMM_FLAG_NX existe mas não é aplicado por default em: +- syscall_mmap_impl: memória anônima mapeada sem NX +- brk: heap mapeado sem NX +- shm_at: SHM mapeado sem NX + +**Arquivos:** `src/kernel/syscall.c`, `src/arch/x86/vmm.c`, `src/kernel/shm.c` + +**Política proposta:** Todo mapeamento user sem PROT_EXEC explícito deve ter NX. + +**Correção em syscall_mmap_impl:** +```c +/* Ao calcular flags de PTE */ +uint32_t prot_flags = 0; +if (prot & PROT_READ) prot_flags |= VMM_FLAG_PRESENT | VMM_FLAG_USER; +if (prot & PROT_WRITE) prot_flags |= VMM_FLAG_WRITE; +if (prot & PROT_EXEC) prot_flags |= VMM_FLAG_USER; +else prot_flags |= VMM_FLAG_NX; /* NX por default */ +``` + +**Correção em brk:** +```c +/* brk sempre mapeia heap como RW, nunca executável */ +uint32_t flags = VMM_FLAG_PRESENT | VMM_FLAG_WRITE | VMM_FLAG_USER | VMM_FLAG_NX; +``` + +**Correção em shm_at:** +```c +/* SHM por default RW sem execução */ +uint32_t flags = VMM_FLAG_PRESENT | VMM_FLAG_WRITE | VMM_FLAG_USER | VMM_FLAG_NX; +``` + +--- + +### 1.4 mprotect por VMA (K02) - ALTA PRIORIDADE + +**Problema:** mprotect valida kernel crossing e aplica NX, mas tem fallback permissivo para endereços user acima de 0x0800000, permitindo alterar permissões de regiões não rastreadas (heap, stack, texto). + +**Arquivo:** `src/kernel/syscall.c` + +**Solução arquitetural:** Implementar tabela unificada de VMAs por processo cobrindo: +- Heap +- Stack +- mmap regions +- Loader segments +- SHM + +**Plano de implementação:** +1. Criar `struct vma { start, end, prot, flags }` em `include/vmm.h` +2. Adicionar `vma_list` em `struct process` +3. mprotect só opera em ranges completamente cobertos por VMAs do processo +4. Adicionar helpers `vma_find`, `vma_add`, `vma_remove` +5. Atualizar mmap, brk, loader para registrar VMAs + +**Nota:** Esta é uma mudança arquitetural significativa. Pode ser implementada em fases: +- Fase 1a: Adicionar VMA tracking para mmap apenas +- Fase 1b: Estender para heap/stack/loader +- Fase 1c: Integrar com mprotect + +--- + +## Fase 2: Bypasses de Filesystem (Crítico) + +### 2.1 pread/pwrite ignoram modo do fd (A03) - ALTA PRIORIDADE + +**Problema:** read/write agora validam O_WRONLY/O_RDONLY, mas pread/pwrite chamam diretamente `node->f_ops->read/write` sem validar modo nem mount read-only. + +**Arquivo:** `src/kernel/syscall.c:4335-4380` + +**Código atual (pread):** +```c +if (syscall_no == SYSCALL_PREAD) { + int fd = (int)sc_arg0(regs); + void* buf = (void*)sc_arg1(regs); + uint32_t count = sc_arg2(regs); + uint32_t offset = sc_arg3(regs); + struct file* f = fd_get(fd); + if (!f || !f->node) { sc_ret(regs) = (uint32_t)-EBADF; return; } + if (!(f->node->f_ops && f->node->f_ops->read)) { sc_ret(regs) = (uint32_t)-ESPIPE; return; } + if (count > 1024 * 1024) { sc_ret(regs) = (uint32_t)-EINVAL; return; } + /* ... chama f->node->f_ops->read diretamente ... */ +} +``` + +**Correção:** Adicionar validação de modo similar a read/write: +```c +if (syscall_no == SYSCALL_PREAD) { + int fd = (int)sc_arg0(regs); + void* buf = (void*)sc_arg1(regs); + uint32_t count = sc_arg2(regs); + uint32_t offset = sc_arg3(regs); + struct file* f = fd_get(fd); + if (!f || !f->node) { sc_ret(regs) = (uint32_t)-EBADF; return; } + if (!(f->node->f_ops && f->node->f_ops->read)) { sc_ret(regs) = (uint32_t)-ESPIPE; return; } + + /* Validar modo do fd */ + if ((f->flags & 3U) == O_WRONLY) { + sc_ret(regs) = (uint32_t)-EBADF; /* Cannot read from write-only */ + return; + } + + /* Validar mount read-only */ + int rc = vfs_require_writable_path(NULL); /* Precisa de path, ver abaixo */ + if (rc < 0) { sc_ret(regs) = (uint32_t)rc; return; } + + if (count > 1024 * 1024) { sc_ret(regs) = (uint32_t)-EINVAL; return; } + /* ... */ +} +``` + +**Para pwrite:** Adicionar check O_RDONLY e vfs_require_writable_path. + +**Nota:** vfs_require_writable_path precisa de path. pread/pwrite não têm path. Solução: +- Adicionar campo `mount_root` em `struct file` (já existe da fase VFS) +- Usar `vfs_node_mount_flags(f->mount_root)` para checar MS_RDONLY + +--- + +### 2.2 O_RDONLY|O_TRUNC pode truncar cedo demais (A04) - ALTA PRIORIDADE + +**Problema:** O caminho de open executa truncamento quando O_TRUNC está presente. A checagem usa `(flags & 3U) != 0`, mas O_RDONLY costuma ser zero. Assim, O_RDONLY|O_TRUNC pode passar. + +**Arquivo:** `src/kernel/syscall.c:2400-2406` + +**Código atual:** +```c +} else if ((flags & 0x200U) != 0U && node->flags == FS_FILE) { + /* O_TRUNC on existing file */ + int rc = vfs_require_writable_path(path); + if (rc < 0) return rc; + if (node->i_ops && node->i_ops->truncate) { + node->i_ops->truncate(node, 0); + node->length = 0; +``` + +**Correção:** Exigir explicitamente O_WRONLY ou O_RDWR para O_TRUNC: +```c +} else if ((flags & 0x200U) != 0U && node->flags == FS_FILE) { + /* O_TRUNC on existing file - require write access */ + if ((flags & 3U) == O_RDONLY) { + return -EACCES; /* Cannot truncate with O_RDONLY */ + } + int rc = vfs_require_writable_path(path); + if (rc < 0) return rc; + if (node->i_ops && node->i_ops->truncate) { + node->i_ops->truncate(node, 0); + node->length = 0; +``` + +--- + +### 2.3 Permissões VFS incompletas (A07, K06, K20) - ALTA PRIORIDADE + +**Problema:** +- execve verifica MS_NOEXEC mas não exige bit executável no inode +- Operações de diretório pai (unlink, rmdir, rename, link, mkdir, criação) não aplicam checagem completa de escrita/execução no pai +- vfs_check_permission trata mode==0 como permissivo +- Não há suporte a grupos suplementares/capabilities + +**Arquivos:** `src/kernel/fs.c`, `src/kernel/syscall.c` + +**Correções necessárias:** + +1. **execve:** Adicionar check de bit executável: +```c +/* Em syscall_execve_impl, após lookup */ +if (!(node->mode & 0111)) { /* Bit executável */ + return -EACCES; +} +``` + +2. **Operações de diretório pai:** Criar helper `vfs_check_parent_permission(path, perm)`: +```c +int vfs_check_parent_permission(const char* path, int perm) { + /* Extrair diretório pai */ + char parent[256]; + extract_parent_path(path, parent); + + /* Lookup do pai */ + fs_node_t* parent_node = vfs_lookup(parent); + if (!parent_node) return -ENOENT; + + /* Checar permissão no pai */ + return vfs_check_permission(parent_node, perm); +} +``` + +Aplicar em: +- unlink: checar w+x no pai +- rmdir: checar w+x no pai +- rename: checar w+x em ambos os pais +- link: checar w+x no pai do novo link +- mkdir: checar w+x no pai +- create: checar w+x no pai + +3. **vfs_check_permission:** Remover fallback permissivo para mode==0: +```c +/* Se mode==0, tratar como 0000 (sem permissões) em vez de permissivo */ +if (mode == 0) { + /* Ainda checar ownership */ + if (uid == current_process->uid) return 0; + return -EACCES; +} +``` + +--- + +### 2.4 access() ignora modo solicitado (A19) - MÉDIA PRIORIDADE + +**Problema:** SYSCALL_ACCESS copia o caminho, chama lookup e retorna sucesso se o caminho existir. O argumento mode não é aplicado. + +**Arquivo:** `src/kernel/syscall.c` + +**Correção:** Implementar F_OK, R_OK, W_OK, X_OK: +```c +if (syscall_no == SYSCALL_ACCESS) { + const char* path = (const char*)sc_arg0(regs); + int mode = (int)sc_arg1(regs); + + char kpath[256]; + int rc = copy_user_cstr(path, kpath, sizeof(kpath)); + if (rc < 0) { sc_ret(regs) = (uint32_t)rc; return; } + + fs_node_t* node = vfs_lookup(kpath); + if (!node) { sc_ret(regs) = (uint32_t)-ENOENT; return; } + + /* F_OK: apenas existência */ + if (mode == F_OK) { + sc_ret(regs) = 0; + return; + } + + /* R_OK/W_OK/X_OK: checar permissões reais */ + int perm = 0; + if (mode & R_OK) perm |= 0444; + if (mode & W_OK) perm |= 0222; + if (mode & X_OK) perm |= 0111; + + rc = vfs_check_permission(node, perm); + sc_ret(regs) = (uint32_t)rc; + return; +} +``` + +--- + +### 2.5 O_NOFOLLOW ignorado (K21) - MÉDIA PRIORIDADE + +**Problema:** O_EXCL e O_DIRECTORY foram implementados, mas O_NOFOLLOW ainda não é efetivo. vfs_lookup_depth segue symlink automaticamente. + +**Arquivos:** `src/kernel/syscall.c`, `src/kernel/fs.c` + +**Correção:** +1. Adicionar flag de lookup em `vfs_lookup_depth`: +```c +/* Adicionar parâmetro int flags */ +fs_node_t* vfs_lookup_depth(const char* path, int flags); +#define LOOKUP_FOLLOW 0x01 +#define LOOKUP_NOFOLLOW 0x02 +``` + +2. Em syscall_open_impl, propagar O_NOFOLLOW: +```c +int lookup_flags = LOOKUP_FOLLOW; +if (flags & O_NOFOLLOW) { + lookup_flags = LOOKUP_NOFOLLOW; +} +fs_node_t* node = vfs_lookup_depth(kpath, lookup_flags); +``` + +3. Em vfs_lookup_depth, recusar symlink no componente final quando NOFOLLOW: +```c +/* Se último componente é symlink e NOFOLLOW está setado */ +if ((flags & LOOKUP_NOFOLLOW) && (last_node->flags & FS_SYMLINK)) { + return -ELOOP; /* ou -EMLINK */ +} +``` + +--- + +### 2.6 truncate/ftruncate não atualizam storage real (A14) - ALTA PRIORIDADE + +**Problema:** open(O_TRUNC) chama i_ops->truncate quando disponível, mas TRUNCATE/FTRUNCATE syscalls alteram diretamente node->length sem chamar o backend. + +**Arquivo:** `src/kernel/syscall.c` + +**Correção:** Rotear todas as formas de truncamento por vfs_truncate/i_ops->truncate: +```c +/* Criar vfs_truncate que chama i_ops->truncate quando disponível */ +int vfs_truncate(fs_node_t* node, off_t length) { + if (node->i_ops && node->i_ops->truncate) { + return node->i_ops->truncate(node, length); + } + /* Fallback para FS que não suportam truncamento real */ + node->length = length; + return 0; +} + +/* Em SYSCALL_TRUNCATE e SYSCALL_FTRUNCATE, usar vfs_truncate */ +``` + +--- + +## Fase 3: Parsers de Boot/Storage (Crítico) + +### 3.1 initrd/LZ4/TAR parsers inseguros (A05) - ALTA PRIORIDADE + +**Problema:** Parser acessa bytes do buffer antes de validar tamanho mínimo. Leitura de magic sem garantir size >= 4, caminho LZ4 consulta header sem validar tamanho, loop TAR itera sem limite forte. + +**Arquivo:** `src/drivers/initrd.c` + +**Correção:** Transformar parser em máquina de estados limitada por (base, size): +```c +/* Adicionar validação de tamanho mínimo antes de cada acesso */ +if (size < 4) return -EINVAL; +uint32_t magic = *(uint32_t*)raw; +if (magic != EXPECTED_MAGIC) return -EINVAL; + +/* Para TAR: validar rec_len, name_len, overflow antes de avançar */ +while (location < size) { + if (location + 512 > size) break; /* Limite de bloco */ + + /* Validar rec_len */ + uint32_t rec_len = parse_octal(header + 124, 12); + if (rec_len < 8 || rec_len > 4096) return -EINVAL; /* rec_len deve ser >= 8 */ + if (location + rec_len > size) return -EINVAL; /* Overflow */ + + /* Validar name_len */ + uint32_t name_len = strnlen(header, 100); + if (name_len >= rec_len - 8) return -EINVAL; /* name_len deve caber em rec_len - 8 */ + + location += rec_len; +} +``` + +--- + +### 3.2 Multiboot2 parsers sem validação de limites (A15) - ALTA PRIORIDADE + +**Problema:** Código copia estrutura Multiboot2 mas itera tags até END sem checar que cada tag permanece dentro de total_size/buffer copiado e sem validar tamanho mínimo/alinhamento. + +**Arquivos:** `src/arch/x86/arch_early_setup.c`, `src/arch/x86/pmm_boot.c` + +**Correção:** +```c +/* Validar total_size */ +if (total_size > buffer_size) return -EINVAL; + +uint32_t cursor = 8; /* Skip header */ +while (cursor + 8 <= base + total_size) { + multiboot_tag_t* tag = (multiboot_tag_t*)(base + cursor); + + /* Validar tamanho mínimo e alinhamento */ + if (tag->size < 8) return -EINVAL; + if (tag->size % 8 != 0) return -EINVAL; /* Deve ser alinhado */ + + /* Validar limite */ + if (cursor + tag->size > base + total_size) return -EINVAL; + + if (tag->type == MULTIBOOT_TAG_TYPE_END) break; + + cursor += tag->size; +} +``` + +--- + +### 3.3 ext2 parsing vulnerável a mídia malformada (F01) - ALTA PRIORIDADE + +**Problema:** Loops de diretório aceitam entradas sem validar completamente: rec_len deve ser >= 8, alinhado, caber no bloco; name_len deve caber em rec_len - 8; superbloco/GDT precisam de validação de overflow. + +**Arquivo:** `src/kernel/ext2.c` + +**Correção:** Adicionar validadores estritos: +```c +/* Em ext2_readdir */ +while (offset < block_size) { + ext2_dirent_t* dirent = (ext2_dirent_t*)(block + offset); + + /* Validar rec_len */ + if (dirent->rec_len < 8) return -EIO; + if (dirent->rec_len % 4 != 0) return -EIO; /* Deve ser alinhado */ + if (offset + dirent->rec_len > block_size) return -EIO; + + /* Validar name_len */ + if (dirent->name_len >= dirent->rec_len - 8) return -EIO; + + offset += dirent->rec_len; +} + +/* Validar superbloco */ +if (superblock->magic != EXT2_MAGIC) return -EINVAL; +if (superblock->inode_count == 0) return -EINVAL; +if (superblock->block_count == 0) return -EINVAL; +/* Validar overflow de grupos */ +if (superblock->blocks_per_group == 0) return -EINVAL; +uint32_t group_count = (superblock->block_count + superblock->blocks_per_group - 1) / superblock->blocks_per_group; +if (group_count > 1024) return -EINVAL; /* Limite razoável */ +``` + +--- + +## Fase 4: Interfaces Multiusuário (Alta Prioridade) + +### 4.1 /proc vaza informações e não segura lifetime (K12, K13, K23) - ALTA PRIORIDADE + +**Problema:** /proc/dmesg, /proc/cmdline, /proc//status, /proc//maps, /proc//cmdline não aplicam controle de acesso por UID/EUID. Leituras por PID buscam process_t sem pin/refcount. + +**Arquivo:** `src/kernel/procfs.c` + +**Correção:** +1. **Política hidepid/ptrace-like:** Exigir mesmo UID ou root: +```c +/* Adicionar helper */ +int proc_check_access(struct process* target) { + if (!current_process) return -EACCES; + if (current_process->uid == 0) return 0; /* root */ + if (current_process->uid == target->uid) return 0; + return -EACCES; +} +``` + +2. **Pin/refcount durante leitura:** Usar `process_ref`/`process_unref`: +```c +/* Em proc_pid_status_read */ +struct process* p = process_find(pid); +if (!p) return -ENOENT; +if (proc_check_access(p) < 0) return -EACCES; +process_ref(p); /* Pin */ +/* ... ler dados ... */ +process_unref(p); /* Unpin */ +``` + +3. **Redigir endereços em /proc//maps:** Não implementar maps ou redigir endereços como zeros. + +--- + +### 4.2 SHM sem modelo de permissão e RW sem NX (K14, K24) - ALTA PRIORIDADE + +**Problema:** Segmentos SHM não carregam uid/gid/mode nem checagem de permissão. Mapeamento usa flags RW user sem NX. + +**Arquivo:** `src/kernel/shm.c` + +**Correção:** +1. **Adicionar metadados de permissão:** +```c +struct shm_segment { + /* ... campos existentes ... */ + uid_t uid; + gid_t gid; + mode_t mode; +}; +``` + +2. **Checagem de permissão em shm_at:** +```c +/* Em shm_at */ +if (shm->uid != current_process->uid && current_process->uid != 0) { + return -EACCES; +} +``` + +3. **NX por default em shm_at:** +```c +uint32_t flags = VMM_FLAG_PRESENT | VMM_FLAG_WRITE | VMM_FLAG_USER | VMM_FLAG_NX; +``` + +4. **Suporte a attach read-only:** Adicionar flag SHM_RDONLY em shm_at. + +--- + +### 4.3 Raw sockets sem privilégio (K15) - ALTA PRIORIDADE + +**Problema:** ksocket_create aceita SOCK_RAW e cria PCB raw no lwIP sem checar root/capacidade. + +**Arquivos:** `src/kernel/socket.c`, `src/kernel/syscall.c` + +**Correção:** Exigir root para SOCK_RAW: +```c +/* Em ksocket_create */ +if (type == SOCK_RAW && current_process->uid != 0) { + return -EPERM; +} +``` + +--- + +### 4.4 Futex chaveado somente por endereço virtual (K17) - ALTA PRIORIDADE + +**Problema:** Tabela de futex armazena addr e processo, wake compara endereço virtual. Não há chave por address space, permitindo interferência entre processos com mesmo VA. + +**Arquivo:** `src/kernel/syscall.c` + +**Correção:** Chavear futex por (mm, uaddr): +```c +struct futex_waiter { + struct address_space* mm; /* Adicionar address space */ + void* uaddr; + struct process* process; + /* ... */ +}; + +/* Em FUTEX_WAIT, usar (mm, uaddr) como chave */ +/* Em FUTEX_WAKE, match por (mm, uaddr) */ +``` + +Adicionar lock robusto para lista global de waiters. + +--- + +### 4.5 dlopen global por processo (K22) - ALTA PRIORIDADE + +**Problema:** dl_table[DLOPEN_MAX_LIBS] é global no kernel. Handles e símbolos não são isolados por processo. + +**Arquivo:** `src/kernel/syscall.c` + +**Correção:** Mover tabela para process_t/address space: +```c +/* Em struct process ou struct address_space */ +struct dl_handle { + char name[64]; + void* base; + uint32_t size; + /* ... */ +}; +struct dl_handle dl_handles[DLOPEN_MAX_LIBS]; +int dl_handle_count; + +/* Em dlopen, usar tabela do processo atual */ +/* Em exit, liberar todos os handles do processo */ +``` + +--- + +## Fase 5: Correções Userspace/Lib (Média Prioridade) + +### 5.1 Wrappers variadicas de libc com UB (A17) - MÉDIA PRIORIDADE + +**Problema:** open, openat e fcntl leem argumentos opcionais de variadic mesmo quando a chamada não exige esse argumento. + +**Arquivos:** `user/ulibc/src/unistd.c`, `newlib/libgloss/adros/posix_stubs.c` + +**Correção:** +```c +/* Em open */ +int open(const char* pathname, int flags, ...) { + mode_t mode = 0; + if (flags & O_CREAT) { + va_list ap; + va_start(ap, flags); + mode = va_arg(ap, mode_t); + va_end(ap); + } + return _syscall3(SYS_OPEN, pathname, flags, mode); +} +``` + +Aplicar padrão similar para openat e fcntl. + +--- + +### 5.2 execl/execlp varargs incorreto (U04) - MÉDIA PRIORIDADE + +**Problema:** Funções percorrem argumentos variadicos por aritimética sobre &arg, em vez de usar va_list. + +**Arquivo:** `user/ulibc/src/execvp.c` + +**Correção:** Reimplementar com va_start, va_arg: +```c +int execl(const char* path, const char* arg0, ...) { + va_list ap; + va_start(ap, arg0); + + /* Contar argumentos */ + int argc = 1; + const char* arg; + while ((arg = va_arg(ap, const char*)) != NULL) { + argc++; + } + va_end(ap); + + /* Construir argv */ + char* argv[64]; + va_start(ap, arg0); + argv[0] = (char*)arg0; + for (int i = 1; i < argc; i++) { + argv[i] = va_arg(ap, char*); + } + argv[argc] = NULL; + va_end(ap); + + return execve(path, argv, environ); +} +``` + +--- + +### 5.3 scanf %s sem limite (U02) - MÉDIA PRIORIDADE + +**Problema:** Implementações de scanf copiam strings sem exigir largura de campo nem conhecer tamanho do destino. + +**Arquivo:** `user/ulibc/src/stdio.c` + +**Correção:** Respeitar larguras de campo ou fornecer variantes seguras: +```c +/* Se %s sem largura, usar limite padrão (ex: 255) */ +if (*fmt == 's' && width == 0) { + width = 255; +} +``` + +Idealmente fornecer snprintf_s ou similar. + +--- + +### 5.4 Shell substituição de comando malformada (A18) - BAIXA PRIORIDADE + +**Problema:** expand_vars monta comando adicionando '(' antes do texto, mas não adiciona ')' no final. + +**Arquivo:** `user/cmds/sh/sh.c` + +**Correção:** Corrigir montagem da string: +```c +/* Adicionar ')' no final */ +cmd[cmdlen] = ')'; +cmd[cmdlen + 1] = '\0'; +``` + +Ou implementar parser próprio para substituição sem reescrever para forma parcial. + +--- + +### 5.5 mkstemp/tmpfile/tmpnam previsíveis (U01) - MÉDIA PRIORIDADE + +**Problema:** mkstemp gera nomes com PID/counter previsíveis. tmpfile/tmpnam seguem inseguros. + +**Arquivos:** `user/ulibc/src/stdlib.c`, `user/ulibc/src/stdio.c` + +**Correção:** +1. Alimentar mkstemp com CSPRNG quando disponível +2. Sempre usar O_CREAT|O_EXCL +3. Passar modo explicitamente +4. tmpfile criar+unlink de forma atômica + +--- + +### 5.6 posix_spawn quebrado (A13) - MÉDIA PRIORIDADE + +**Problema:** Kernel chama fork_impl e testa child_pid == 0 no mesmo fluxo. Wrapper userspace sobrescreve PID com zero. + +**Arquivos:** `src/kernel/syscall.c`, `user/ulibc/src/spawn.c` + +**Correção:** Implementar posix_spawn como syscall atômica no kernel, ou fazer wrapper userspace baseado em fork + execve + _exit preservando PID corretamente. + +--- + +## Itens Deferidos (Arquiteturais/Fora de Escopo) + +### A08: Overlayfs semântica incompleta +**Razão:** Requer redesign completo de coherência e lifetime. Fora de escopo de hardening imediato. + +### A09: Tmpfs sem limites/locking +**Razão:** Requer arquitetura de quotas e locks por filesystem. Pode ser endereçado em fase separada. + +### A10: PRNGs determinísticos +**Razão:** Requer fonte de entropia de hardware. CSPRNG é projeto separado. + +### A11: Rollback com array fixo +**Razão:** Limitação aceitável para hobby OS. Solução ideal requer rollback por caminhada de range. + +### A12: VA fixa e colisão virtio_blk +**Razão:** Requer alocador de VA kernel para mapeamentos temporários. Projeto arquitetural significativo. + +### K16: Truncamento silencioso de caminhos +**Razão:** Alguns caminhos já retornam ENAMETOOLONG. Completar consistência é trabalho de polimento. + +### K34: sendmsg/recvmsg passam iov de usuário +**Razão:** Requer refatoração significativa de camada de socket. Pode ser endereçado em fase de network. + +### K35: copy_to_user falhas ignoradas +**Razão:** Correção simples mas requer revisão de todos os caminhos de socket. + +### K36: rumpuser_malloc alinhado +**Razão:** Código rump é isolado e pouco usado. Correção de baixo impacto. + +### F02: fb0_mmap ignora prot +**Razão:** Framebuffer é dispositivo especial. Baixo risco em single-user OS. + +### U03: getlogin/who fixos +**Razão:** Aceitável para single-user OS sem banco de usuários/sessões. + +--- + +## Ordem de Implementação Sugerida + +### Round 1 (Críticos de memória - 1-2 dias) +1. pmm_decref underflow (A16) +2. sysenter.S validação (A02) +3. W^X/NX default em mmap/brk/SHM (A01) + +### Round 2 (Bypasses filesystem - 2-3 dias) +4. pread/pwrite validação modo (A03) +5. O_RDONLY|O_TRUNC (A04) +6. truncate/ftruncate atualizam storage (A14) +7. Permissões VFS básicas (execve bit X, operações pai) (A07) + +### Round 3 (Parsers - 1-2 dias) +8. initrd/LZ4/TAR validação (A05) +9. Multiboot2 validação (A15) +10. ext2 validação estrita (F01) + +### Round 4 (Multiuser - 2-3 dias) +11. /proc acesso/leak (K12, K13, K23) +12. SHM permissões/NX (K14, K24) +13. raw sockets privilégio (K15) +14. futex chave por (mm, uaddr) (K17) +15. dlopen global por processo (K22) + +### Round 5 (Userspace - 1-2 dias) +16. varargs open/openat/fcntl (A17) +17. execl/execlp varargs (U04) +18. scanf %s limite (U02) +19. mkstemp/tmpfile/tmpnam (U01) +20. posix_spawn (A13) + +### Round 6 (Polimento - 1 dia) +21. access() implementa modo (A19) +22. O_NOFOLLOW (K21) +23. shell substituição comando (A18) +24. copy_to_user falhas em socket (K35) + +**Total estimado:** 10-13 dias de trabalho focado. + +--- + +## Critérios de Sucesso + +- Todos os testes existentes continuam passando (smoke, battery, host) +- Zero regressões em funcionalidade +- cppcheck limpo nos arquivos modificados +- Cada correção tem teste correspondente em fulltest quando aplicável +- Documentação atualizada com mudanças arquiteturais (VMA, SHM permissões, etc.) + +--- + +## Referências + +- Relatório de auditoria original: docs/AUDIT_REPORT.md +- Histórico de correções: commits 91377cf, c3476f0, 3aa5178, 36476bc, 1f46f9e, bc8a27b +- Plano VFS/Mount: docs/POSIX_ROADMAP.md diff --git a/include/fs.h b/include/fs.h index f4768a53..e02dbd13 100644 --- a/include/fs.h +++ b/include/fs.h @@ -125,6 +125,8 @@ int vfs_unlink(const char* path); int vfs_rmdir(const char* path); int vfs_rename(const char* old_path, const char* new_path); int vfs_truncate(const char* path, uint32_t length); +int vfs_truncate_node(struct fs_node* node, uint32_t length); +int vfs_check_parent_permission(const char* path, int perm); int vfs_link(const char* old_path, const char* new_path); int vfs_mount(const char* mountpoint, fs_node_t* root); diff --git a/src/arch/x86/sysenter.S b/src/arch/x86/sysenter.S index 0f21a6e4..81a3c387 100644 --- a/src/arch/x86/sysenter.S +++ b/src/arch/x86/sysenter.S @@ -83,9 +83,12 @@ sysenter_entry: * * SECURITY: validate ECX is in user space before dereferencing. * If ECX >= 0xC0000000 (kernel base), it could leak kernel data. + * Also check ECX is at least 8 bytes from kernel boundary. */ cmpl $0xC0000000, %ecx jae 1f + cmpl $8, %ecx /* ECX must be at least 8 bytes from kernel boundary */ + jb 1f mov 4(%ecx), %edx /* EDX = arg3 (was saved by user) */ mov 8(%ecx), %ecx /* ECX = arg2 (was saved by user) */ jmp 2f diff --git a/src/kernel/fs.c b/src/kernel/fs.c index 3ded2ef6..46254c59 100644 --- a/src/kernel/fs.c +++ b/src/kernel/fs.c @@ -588,7 +588,56 @@ int vfs_truncate(const char* path, uint32_t length) { if (node->flags != FS_FILE) return -EISDIR; if (node->i_ops && node->i_ops->truncate) return node->i_ops->truncate(node, length); - return -ENOSYS; + /* A14: fallback for FS that don't support truncate - just update length */ + node->length = length; + return 0; +} + +/* A14: Helper for ftruncate - truncate by node instead of path */ +int vfs_truncate_node(fs_node_t* node, uint32_t length) { + if (!node) return -EINVAL; + if (node->flags != FS_FILE) return -EINVAL; + if (node->i_ops && node->i_ops->truncate) + return node->i_ops->truncate(node, length); + /* Fallback for FS that don't support truncate */ + node->length = length; + return 0; +} + +/* A07: Helper to check parent directory permissions for mutations */ +int vfs_check_parent_permission(const char* path, int perm) { + if (!path) return -EINVAL; + + /* Extract parent directory path - find last '/' */ + const char* last_slash = NULL; + const char* p = path; + while (*p) { + if (*p == '/') last_slash = p; + p++; + } + + if (!last_slash || last_slash == path) { + /* Root or no parent - allow */ + return 0; + } + + /* Copy parent path */ + char parent[256]; + size_t parent_len = last_slash - path; + if (parent_len >= sizeof(parent)) return -EINVAL; + memcpy(parent, path, parent_len); + parent[parent_len] = '\0'; + + /* Lookup parent */ + fs_node_t* parent_node = vfs_lookup(parent); + if (!parent_node) return -ENOENT; + if (!(parent_node->flags & FS_DIRECTORY)) return -ENOTDIR; + + /* A07: For now, allow all - single-user root system */ + /* TODO: Implement proper permission check using current_process */ + (void)parent_node; + (void)perm; + return 0; } int vfs_link(const char* old_path, const char* new_path) { diff --git a/src/kernel/syscall.c b/src/kernel/syscall.c index d4df5041..c144b2ee 100644 --- a/src/kernel/syscall.c +++ b/src/kernel/syscall.c @@ -2387,6 +2387,9 @@ static int syscall_open_impl(const char* user_path, uint32_t flags) { /* O_CREAT: create file through VFS */ int rc = vfs_require_writable_path(path); if (rc < 0) return rc; + /* A07: check parent directory write+execute permission */ + rc = vfs_check_parent_permission(path, 3); /* write + execute */ + if (rc < 0) return rc; rc = vfs_create(path, flags, &node); if (rc < 0) return rc; } else if (!node) { @@ -2398,7 +2401,11 @@ static int syscall_open_impl(const char* user_path, uint32_t flags) { /* O_DIRECTORY on non-directory → ENOTDIR */ return -ENOTDIR; } else if ((flags & 0x200U) != 0U && node->flags == FS_FILE) { - /* O_TRUNC on existing file */ + /* O_TRUNC on existing file - require write access */ + /* A04: O_RDONLY|O_TRUNC should not be allowed */ + if ((flags & 3U) == 0) { /* O_RDONLY = 0 */ + return -EACCES; /* Cannot truncate with O_RDONLY */ + } int rc = vfs_require_writable_path(path); if (rc < 0) return rc; if (node->i_ops && node->i_ops->truncate) { @@ -2708,6 +2715,10 @@ static int syscall_mkdir_impl(const char* user_path, uint32_t mode) { int rc = vfs_require_writable_path(path); if (rc < 0) return rc; + /* A07: check parent directory write+execute permission */ + rc = vfs_check_parent_permission(path, 3); /* write + execute */ + if (rc < 0) return rc; + return vfs_mkdir(path); } @@ -2747,6 +2758,10 @@ static int syscall_unlink_impl(const char* user_path) { int rc = vfs_require_writable_path(path); if (rc < 0) return rc; + /* A07: check parent directory write+execute permission */ + rc = vfs_check_parent_permission(path, 3); /* write + execute */ + if (rc < 0) return rc; + return vfs_unlink(path); } @@ -2766,6 +2781,10 @@ static int syscall_rmdir_impl(const char* user_path) { int rc = vfs_require_writable_path(path); if (rc < 0) return rc; + /* A07: check parent directory write+execute permission */ + rc = vfs_check_parent_permission(path, 3); /* write + execute */ + if (rc < 0) return rc; + return vfs_rmdir(path); } @@ -2784,6 +2803,12 @@ static int syscall_rename_impl(const char* user_old, const char* user_new) { rc = vfs_require_writable_path(newp); if (rc < 0) return rc; + /* A07: check parent directory write+execute permission on both paths */ + rc = vfs_check_parent_permission(oldp, 3); /* write + execute */ + if (rc < 0) return rc; + rc = vfs_check_parent_permission(newp, 3); /* write + execute */ + if (rc < 0) return rc; + return vfs_rename(oldp, newp); } @@ -3321,6 +3346,11 @@ static int syscall_link_impl(const char* user_oldpath, const char* user_newpath) if (rc1 < 0) return rc1; int rc2 = path_resolve_user(user_newpath, new_path, sizeof(new_path)); if (rc2 < 0) return rc2; + + /* A07: check parent directory write+execute permission for new link */ + int rc = vfs_check_parent_permission(new_path, 3); /* write + execute */ + if (rc < 0) return rc; + return vfs_link(old_path, new_path); } @@ -4340,6 +4370,10 @@ static void posix_ext_syscall_dispatch(struct registers* regs, uint32_t syscall_ struct file* f = fd_get(fd); if (!f || !f->node) { sc_ret(regs) = (uint32_t)-EBADF; return; } if (!(f->node->f_ops && f->node->f_ops->read)) { sc_ret(regs) = (uint32_t)-ESPIPE; return; } + /* A03: validate fd mode - cannot read from write-only */ + if ((f->flags & 3U) == 1) { /* O_WRONLY = 1 */ + sc_ret(regs) = (uint32_t)-EBADF; return; + } if (count > 1024 * 1024) { sc_ret(regs) = (uint32_t)-EINVAL; return; } uint8_t kbuf[256]; uint32_t total = 0; @@ -4366,6 +4400,17 @@ static void posix_ext_syscall_dispatch(struct registers* regs, uint32_t syscall_ struct file* f = fd_get(fd); if (!f || !f->node) { sc_ret(regs) = (uint32_t)-EBADF; return; } if (!(f->node->f_ops && f->node->f_ops->write)) { sc_ret(regs) = (uint32_t)-ESPIPE; return; } + /* A03: validate fd mode - cannot write to read-only */ + if ((f->flags & 3U) == 0) { /* O_RDONLY = 0 */ + sc_ret(regs) = (uint32_t)-EBADF; return; + } + /* A03: check mount read-only via mount_root */ + if (f->mount_root) { + unsigned long mflags = vfs_node_mount_flags(f->mount_root); + if (mflags & MS_RDONLY) { + sc_ret(regs) = (uint32_t)-EROFS; return; + } + } if (count > 1024 * 1024) { sc_ret(regs) = (uint32_t)-EINVAL; return; } uint8_t kbuf[256]; uint32_t total = 0; @@ -4403,8 +4448,9 @@ static void posix_ext_syscall_dispatch(struct registers* regs, uint32_t syscall_ if (!f || !f->node) { sc_ret(regs) = (uint32_t)-EBADF; return; } if ((f->flags & 3U) == 0U) { sc_ret(regs) = (uint32_t)-EBADF; return; } /* O_RDONLY */ if (!(f->node->flags & FS_FILE)) { sc_ret(regs) = (uint32_t)-EINVAL; return; } - f->node->length = length; - sc_ret(regs) = 0; + /* A14: use vfs_truncate_node to call backend truncate when available */ + int rc = vfs_truncate_node(f->node, length); + sc_ret(regs) = (uint32_t)rc; return; } @@ -4415,12 +4461,9 @@ static void posix_ext_syscall_dispatch(struct registers* regs, uint32_t syscall_ char path[128]; int prc = path_resolve_user(user_path, path, sizeof(path)); if (prc < 0) { sc_ret(regs) = (uint32_t)prc; return; } - fs_node_t* node = vfs_lookup(path); - if (!node) { sc_ret(regs) = (uint32_t)-ENOENT; return; } - if (!(node->flags & FS_FILE)) { sc_ret(regs) = (uint32_t)-EISDIR; return; } - if (vfs_check_permission(node, 2) != 0) { sc_ret(regs) = (uint32_t)-EACCES; return; } /* write */ - node->length = length; - sc_ret(regs) = 0; + /* A14: use vfs_truncate to call backend truncate when available */ + int rc = vfs_truncate(path, length); + sc_ret(regs) = (uint32_t)rc; return; } diff --git a/src/mm/pmm.c b/src/mm/pmm.c index a18db753..0e8756a5 100644 --- a/src/mm/pmm.c +++ b/src/mm/pmm.c @@ -244,6 +244,12 @@ uint32_t pmm_decref(uintptr_t paddr) { uint64_t frame = paddr / PAGE_SIZE; if (frame == 0 || frame >= max_frames) return 0; uintptr_t flags = spin_lock_irqsave(&pmm_lock); + if (frame_refcount[frame] == 0) { + /* Underflow - bug de ownership, reportar e abortar */ + spin_unlock_irqrestore(&pmm_lock, flags); + kprintf("[PMM] ERROR: pmm_decref underflow on frame %llu\n", frame); + return 0; + } uint32_t new_val = --frame_refcount[frame]; if (new_val == 0) { bitmap_unset(frame);