1.. SPDX-License-Identifier: GPL-2.0 2 3============================================ 4(Como evitar) Deixar as ioctls malfeitas 5============================================ 6 7De: https://blog.ffwll.ch/2013/11/botching-up-ioctls.html 8 9Por: Daniel Vetter, Copyright © 2013 Intel Corporation 10 11Uma percepção clara que os hackers de gráficos do kernel tiveram nos últimos 12anos é que tentar criar uma interface unificada para gerenciar as unidades de 13execução e a memória em GPUs completamente diferentes é um esforço inútil. 14Portanto, hoje em dia, cada driver tem seu próprio conjunto de ioctls para 15alocar memória e enviar trabalho para a GPU. O que é bom, já que não há mais a 16insanidade na forma de interfaces falsamente genéricas, mas que na verdade só 17são usadas uma vez. No entanto, a desvantagem clara é que há muito mais 18potencial para estragar as coisas. 19 20Para evitar repetir todos os mesmos erros novamente, escrevi algumas das lições 21aprendidas enquanto fazia um trabalho malfeito para o driver drm/i915. A maioria 22delas aborda apenas tecnicalidades e não os problemas macro (big-picture), como 23deveria ser exatamente a aparência da ioctl de envio de comando. Aprender essas 24lições é provavelmente algo que cada driver de GPU tem que fazer por conta 25própria. 26 27 28Pré-requisitos 29-------------- 30 31Primeiro, os pré-requisitos. Sem estes você já falhou, porque precisará 32adicionar uma camada de compatibilidade de 32 bits (compat layer): 33 34 * Use apenas inteiros de tamanho fixo. Para evitar conflitos com typedefs no 35 espaço de usuário (userspace), o kernel possui tipos especiais como __u32 e 36 __s64. Use-os. 37 38 * Alinhe tudo ao tamanho natural e use preenchimento (padding) explícito. 39 Plataformas de 32 bits não alinham necessariamente valores de 64 bits a 40 limites (boundaries) de 64 bits, mas plataformas de 64 bits o fazem. Portanto, 41 sempre precisamos de padding para o tamanho natural para acertar isso. 42 43 * Preencha a struct inteira para um múltiplo de 64 bits se a estrutura contiver 44 tipos de 64 bits -- caso contrário, o tamanho da estrutura diferirá entre 45 32 bits e 64 bits. Ter um tamanho de estrutura diferente prejudica ao passar 46 matrizes (arrays) de estruturas para o kernel, ou se o kernel verificar o 47 tamanho da estrutura, o que o core do drm, por exemplo, faz. 48 49 * Ponteiros são __u64, convertidos de/para um uintptr_t no lado do espaço de 50 usuário e de/para um void __user * no kernel. Tente de verdade não atrasar 51 essa conversão ou, pior ainda, manipular o __u64 bruto pelo seu código, pois 52 isso diminui a verificação que ferramentas como o sparse podem fornecer. A 53 macro u64_to_user_ptr pode ser usada no kernel para evitar avisos sobre 54 inteiros e ponteiros de tamanhos diferentes. 55 56 57Conceitos básicos 58----------------- 59 60Evitadas as alegrias de escrever uma camada de compatibilidade (compat layer), 61podemos dar uma olhada nos deslizes básicos. Negligenciar estes pontos tornará a 62compatibilidade retroativa e futura uma verdadeira dor de cabeça. E, como errar 63na primeira tentativa é garantido, você certamente terá uma segunda iteração ou, 64pelo menos, uma extensão para qualquer interface fornecida. 65 66 * Tenha uma maneira clara para o espaço de usuário descobrir se a sua nova 67 ioctl ou extensão de ioctl é suportada em um determinado kernel. Se você não 68 puder confiar que os kernels antigos rejeitarão as novas flags/modos ou 69 ioctls (já que fazer isso foi deixado de lado no passado), então você 70 precisará de uma flag de recurso (feature flag) do driver ou de um número de 71 revisão em algum lugar. 72 73 * Tenha um plano para estender as ioctls com novas flags ou novos campos no 74 final da estrutura. O core do drm verifica o tamanho passado para cada 75 chamada de ioctl e preenche com zero (zero-extends) quaisquer divergências 76 entre o kernel e o espaço de usuário. Isso ajuda, mas não é uma solução 77 completa, já que um espaço de usuário mais novo em um kernel mais antigo não 78 notará que os campos recém-adicionados no final estão sendo ignorados. 79 Portanto, isso ainda exige novas flags de recurso do driver. 80 81 * Verifique todos os campos e flags não utilizados, além de todo o preenchimento 82 (padding), para garantir que estejam em 0, e rejeite a ioctl se esse não for 83 o caso. Caso contrário, seu excelente plano para extensões futuras irá por 84 água abaixo, pois alguém enviará uma struct de ioctl com lixo de pilha 85 (stack garbage) aleatório nas partes ainda não utilizadas. O que, então, 86 consolida na ABI que esses campos nunca poderão ser usados para nada além de 87 lixo. Esta também é a razão pela qual você deve preencher explicitamente todas 88 as estruturas, mesmo que nunca as use em uma matriz (array) -- o padding que 89 o compilador possa inserir poderia conter lixo. 90 91 * Tenha casos de teste simples para tudo o que foi mencionado acima. 92 93 94Diversão com caminhos de erro (Error Paths) 95------------------------------------------- 96 97Hoje em dia, não temos mais nenhuma desculpa para que os drivers drm sejam pequenos 98exploits de root disfarçados. Isso significa que precisamos tanto de uma 99validação completa de entrada quanto de caminhos sólidos de tratamento de erros 100-- as GPUs eventualmente vão parar de funcionar (die) nos casos mais bizarros 101de qualquer maneira: 102 103 * A ioctl deve verificar se há estouros de matriz (array overflows). Ela também 104 precisa verificar estouros superiores/inferiores (over/underflows) e problemas 105 de limitação (clamping) de valores inteiros em geral. O exemplo usual são os 106 valores de posicionamento de sprite alimentados diretamente no hardware, onde 107 o hardware possui apenas 12 bits ou algo assim. Funciona perfeitamente até que 108 algum servidor de exibição bizarro não se preocupe em fazer o clamping por si 109 mesmo e o cursor dê a volta (wrap around) na tela. 110 111 * Tenha casos de teste simples para cada caso de falha de validação de entrada 112 na sua ioctl. Verifique se o código de erro corresponde às suas expectativas. 113 E, finalmente, certifique-se de testar apenas um único caminho de erro em 114 cada subteste, enviando dados que, de outra forma, seriam perfeitamente 115 válidos. Sem isso, uma verificação anterior já poderia rejeitar a ioctl e 116 ofuscar (shadow) o caminho de código que você realmente deseja testar, 117 ocultando bugs e regressões. 118 119 * Torne todas as suas ioctls reiniciáveis (restartable). Primeiro, o X (X11) 120 realmente ama sinais (signals) e, segundo, isso permitirá que você teste 90% 121 de todos os caminhos de tratamento de erro apenas interrompendo sua suíte de 122 testes principal constantemente com sinais. Graças ao amor do X por sinais, 123 você obterá uma excelente cobertura de base de todos os seus caminhos de erro 124 praticamente de graça para drivers de gráficos. Além disso, seja consistente 125 na forma como você lida com a reinicialização de ioctls -- por exemplo, o drm 126 possui um pequeno helper drmIoctl em sua biblioteca de espaço de usuário. O 127 driver i915 estragou isso com a ioctl set_tiling; agora estamos presos para 128 sempre com algumas semânticas arcanas tanto no kernel quanto no espaço de 129 usuário. 130 131 * Se você não puder tornar um determinado caminho de código reiniciável, torne 132 uma tarefa travada pelo menos finalizável (killable). As GPUs simplesmente 133 morrem, e seus usuários não vão gostar mais de você se você travar a máquina 134 inteira deles (por meio de um processo do X impossível de matar). Se a 135 recuperação de estado ainda for muito complicada, tenha um timeout ou uma 136 rede de segurança de verificação de travamento (hangcheck) como um esforço de 137 última hora (last-ditch) caso o hardware enlouqueça (gone bananas). 138 139 * Tenha casos de teste para os cenários mais complexos (corner cases) no seu 140 código de recuperação de erros -- é fácil demais criar um deadlock entre seu 141 código de hangcheck e os processos que estão aguardando (waiters). 142 143 144Tempo, Espera e a Perda de Prazos 145--------------------------------- 146 147As GPUs fazem quase tudo de forma assíncrona, portanto, temos a necessidade de 148cronometrar operações e aguardar pelas que estão pendentes. Esse é um negócio 149realmente complicado; no momento, nenhuma das ioctls suportadas pelo drm/i915 150acerta isso completamente, o que significa que ainda há toneladas de lições para 151aprender aqui. 152 153 * Use CLOCK_MONOTONIC como seu tempo de referência, sempre. É o que o alsa, o 154 drm e o v4l usam por padrão hoje em dia. Mas informe ao espaço de usuário 155 quais carimbos de data/hora (timestamps) são derivados de domínios de relógio 156 diferentes, como o relógio principal do seu sistema (fornecido pelo kernel) 157 ou algum contador de hardware independente em outro lugar. Os relógios vão 158 divergir se você olhar de perto o suficiente, mas se as ferramentas de 159 medição de desempenho tiverem essa informação, elas poderão ao menos compensar. 160 Se o seu espaço de usuário puder obter os valores brutos de alguns relógios 161 (por exemplo, por meio de instruções de amostragem de contador de desempenho 162 no fluxo de comandos), considere expor esses também. 163 164 * Use __s64 para segundos mais __u64 para nanossegundos para especificar o 165 tempo. Não é a especificação de tempo mais conveniente, mas é praticamente o 166 padrão. 167 168 * Verifique se os valores de tempo de entrada estão normalizados e rejeite-os 169 caso contrário. Note que a struct nativa do kernel, ktime, possui um inteiro 170 sinalizado tanto para segundos quanto para nanossegundos, portanto, cuidado 171 aqui. 172 173 * Para timeouts, use tempos absolutos. Se você for um bom sujeito e tiver 174 tornado a sua ioctl reiniciável, os timeouts relativos tendem a ser muito 175 imprecisos (coarse) e podem estender indefinidamente o seu tempo de espera 176 devido ao arredondamento a cada reinicialização. Especialmente se o seu relógio 177 de referência for algo realmente lento, como o contador de quadros da tela 178 (display frame counter). Vestindo o chapéu de advogado de especificações, isso 179 não é um bug, já que os timeouts sempre podem ser estendidos -- mas os usuários 180 com certeza vão odiar você se as belas animações deles começarem a gaguejar 181 (stutter) devido a isso. 182 183 * Considere descartar quaisquer ioctls de espera síncrona com timeouts e apenas 184 entregue um evento assíncrono em um descritor de arquivo passível de poll 185 (pollable file descriptor). Isso se encaixa muito melhor no loop principal de 186 aplicações orientadas a eventos. 187 188 * Tenha casos de teste para cenários complexos (corner-cases), especialmente se 189 os valores de retorno para eventos já concluídos, esperas bem-sucedidas e 190 esperas que estouraram o tempo (timed-out) são todos sãos e adequados às suas 191 necessidades. 192 193 194Evitando o vazamento de recursos (Leaking Resources, Not) 195--------------------------------------------------------- 196 197Um driver drm completo essencialmente implementa um pequeno SO, mas especializado 198para as plataformas de GPU fornecidas. Isso significa que um driver precisa 199expor toneladas de handles (identificadores) para diferentes objetos e outros 200recursos para o espaço de usuário. Fazer isso corretamente traz seu próprio 201pequeno conjunto de armadilhas: 202 203 * Sempre vincule o tempo de vida (lifetime) de seus recursos criados 204 dinamicamente ao tempo de vida de um descritor de arquivo (file descriptor - 205 fd). Considere usar um mapeamento 1:1 se o seu recurso precisar ser 206 compartilhado entre processos -- a passagem de fds sobre unix domain sockets 207 também simplifica o gerenciamento do tempo de vida para o espaço de usuário. 208 209 * Sempre tenha suporte a O_CLOEXEC. 210 211 * Certifique-se de que você tem isolamento suficiente entre os diferentes 212 clientes. Por padrão, escolha um namespace privado por fd, o que força 213 qualquer compartilhamento a ser feito de forma explícita. Só adote um 214 namespace mais global por dispositivo se os objetos forem verdadeiramente 215 únicos do dispositivo. Um contraexemplo nas interfaces de modeset do drm é 216 que os objetos de modeset por dispositivo, como conectores, compartilham um 217 namespace com objetos de framebuffer, que na maioria das vezes não são 218 compartilhados de forma alguma. Um namespace separado, privado por padrão, 219 para os framebuffers teria sido mais adequado. 220 221 * Pense sobre os requisitos de unicidade para os handles do espaço de usuário. 222 Por exemplo, para a maioria dos drivers drm, é um bug do espaço de usuário 223 enviar o mesmo objeto duas vezes na mesma ioctl de envio de comando. Mas, 224 se os objetos forem compartilháveis, o espaço de usuário precisa saber se 225 já viu um objeto importado de outro processo ou não. Eu ainda não tentei isso 226 sozinho devido à falta de uma nova classe de objetos, mas considere usar 227 números de inode em seus descritores de arquivo compartilhados como 228 identificadores únicos -- é assim que arquivos reais também são diferenciados. 229 Infelizmente, isso requer um sistema de arquivos virtual completo no kernel. 230 231 232Por último, mas não menos importante 233------------------------------------ 234 235Nem todo problema precisa de uma nova ioctl: 236 237 * Pense bem se você realmente quer uma interface privada do driver. Claro que 238 é muito mais rápido aprovar uma interface privada do driver do que se envolver 239 em discussões longas por uma solução mais genérica. E, ocasionalmente, criar 240 uma interface privada para liderar um novo conceito é o que se exige. Mas, 241 no final, assim que a interface genérica surgir, você acabará mantendo duas 242 interfaces. Indefinidamente. 243 244 * Considere outras interfaces além de ioctls. Um atributo sysfs é muito melhor 245 para configurações por dispositivo ou para objetos filhos com tempos de vida 246 razoavelmente estáticos (como conectores de saída no drm com todos os seus 247 atributos de sobreposição de detecção). Ou talvez apenas a sua suíte de 248 testes precise dessa interface e, nesse caso, o debugfs, com seu aviso de 249 isenção de responsabilidade por não ter uma ABI estável, seria melhor. 250 251Finalmente, o objetivo principal é acertar na primeira tentativa, pois se o seu 252driver se provar popular e suas plataformas de hardware forem duradouras, você 253ficará preso a uma determinada ioctl essencialmente para sempre. Você pode 254tentar depreciar ioctls horríveis em iterações mais novas do seu hardware, mas 255geralmente leva anos para conseguir isso. E depois mais anos até que o último 256usuário capaz de reclamar sobre regressões desapareça também.