Logo NascimentoIgor Nascimento / Blog
Sistemas

Tela cinza, monitor que apaga e uma linha de C: consertando um driver USB de vídeo sem tocar no binário fechado

Um adaptador USB de vídeo WAVLINK, Ubuntu 24.04 com Wayland, e três sintomas: o instalador oficial aborta, o desktop sobe numa tela cinza congelada e o monitor acende e apaga ao ser conectado. Uma conversa sobre como achar a camada que você tem o direito de mudar, sobre um strlen(NULL) escondido num wrapper descontinuado, e sobre por que a parte mais trabalhosa do projeto foi tornar cada passo reversível.

SistemasLinux15 min de leituraIgor Nascimento

O problema, em três sintomas

Eu precisava ligar mais de três monitores no notebook, e descobri uma coisa frustrante: quase nenhum adaptador USB de vídeo tem suporte real para Linux. Windows e Mac, sempre. Linux, quase nunca. Foram três dias de pesquisa até achar um que prometia funcionar. Comprei aliviado. E aí fui instalar o driver oficial.

O hardware é um adaptador USB de vídeo WAVLINK, modelo WL-UG7602HC, baseado no chip Silicon Motion SM768. A máquina é um notebook com Ubuntu 24.04, GNOME em Wayland e GPU AMD. O fabricante distribui um driver oficial para Linux, e com ele o que se obtém é isto:

  1. O instalador oficial aborta. Termina com Failed to install evdi ... to the kernel e bad exit status: 2, desfaz o que tinha feito e nada fica instalado.
  2. Se o instalador passa, o desktop sobe numa tela cinza congelada. Só se entra pelo modo de recuperação.
  3. Ao conectar o monitor USB, ele acende, pisca o logotipo e apaga. O desktop nunca aparece nele.

Eu poderia ter devolvido o produto e desistido. Seria o mais fácil. Resolvi entender o problema, e a virada de chave veio de uma constatação: no fundo, todos esses adaptadores funcionam da mesma maneira, criando telas virtuais por uma camada aberta, licenciada em GPL. Se o problema estivesse ali, dava para corrigir.

Três sintomas diferentes, e a tentação inicial é tratar como um único "o driver não funciona". Não é. São três causas distintas, em momentos distintos: uma no momento de instalar, duas depois. E a descoberta que organiza o projeto inteiro é que as três estão na camada aberta, nenhuma no firmware nem no daemon fechado do fabricante. Isso é o que torna o problema resolvível sem engenharia reversa, e é a primeira coisa que vale aprender aqui: antes de atacar, mapear quais camadas você tem o direito de mudar.

O repositório com a correção completa está em wavlink_driver_ubuntu, com documentação em inglês e em português.

A arquitetura que o fabricante monta

Para entender as causas é preciso saber como um adaptador desses funciona no Linux. O chip não é uma GPU; ele é um dispositivo USB que recebe quadros comprimidos e os exibe. Para que o compositor de janelas enxergue isso como um monitor, usa-se o EVDI (Extensible Virtual Display Interface), um projeto de código aberto da DisplayLink com duas partes: um módulo de kernel, que cria placas de vídeo virtuais, e uma biblioteca em espaço de usuário, a libevdi, que entrega os quadros dessas placas virtuais para quem quiser. O driver do fabricante é um daemon fechado, o SMIUSBDisplayManager, que usa a libevdi para pegar os quadros e os manda pelo USB para o chip.

A pilha, portanto, é: compositor, módulo evdi no kernel, libevdi, daemon fechado, USB, chip. O fabricante empacota tudo num instalador .run, inclusive uma cópia do código-fonte do EVDI, que é compilada na máquina do usuário via DKMS. E é nessa cópia que tudo começa a dar errado.

Causa zero: o instalador compila um EVDI velho demais

O instalador oficial embute o EVDI 1.14.7, de novembro de 2024. O Ubuntu 24.04 atualizado roda kernels da série 6.8 em diante (a máquina de teste estava no 6.17), e a API interna de DRM do kernel mudou nesse intervalo. A compilação do módulo falha, o DKMS devolve o bad exit status: 2, e o instalador, que é bem comportado nesse ponto, faz rollback. O resultado é um sistema limpo e um driver que não existe.

A correção é trocar a cópia embutida pelo EVDI 1.15.0, que compila nos kernels atuais. O script prepare-vendor-driver.sh faz isso sem rodar o instalador do fabricante: extrai o .run com a opção que impede a execução, substitui o evdi.tar.gz interno, e escreve uma cópia corrigida ao lado da original. O usuário instala a partir da cópia. O pacote original fica intocado.

Uma decisão que parece detalhe: rodar offline

A versão inicial do script baixava o EVDI 1.15.0 do GitHub. Isso foi trocado por um tarball empacotado no próprio repositório, e a razão é prática: quem está rodando esse script está, por definição, com um monitor que não funciona, às vezes numa máquina em modo de recuperação, nem sempre com rede. Um passo de reparo que depende de rede falha exatamente quando é mais necessário. O único arquivo que o usuário precisa baixar é o driver fechado, que não pode ser redistribuído.

Há um ponto de Secure Boot que costuma assustar e aqui não exige nada: o DKMS assina o módulo com a chave MOK que o Ubuntu já tem registrada na máquina, a mesma que usa para o VirtualBox e outros módulos. Não se cria nem se registra chave nova.

Causa um: um strlen(NULL) num wrapper descontinuado

Com o driver instalado, conectar o monitor matava o daemon do fabricante com SIGSEGV. Toda vez. A investigação levou a uma função da libevdi marcada como descontinuada, evdi_open_attached_to, que o daemon fechado ainda chama passando NULL como argumento para pedir um dispositivo genérico. Na versão 1.15 da biblioteca, esse wrapper virou uma linha que chama a versão nova, evdi_open_attached_to_fixed, passando o comprimento da string. E para calcular o comprimento, chama strlen no ponteiro. Com NULL, isso é um acesso ao endereço zero, e o processo morre antes de a função nova, que trata NULL corretamente, receber o ponteiro.

A correção é uma linha:

// deprecated, use evdi_open_attached_to_fixed
evdi_handle evdi_open_attached_to(const char *sysfs_parent_device)
{
    return evdi_open_attached_to_fixed(sysfs_parent_device,
        sysfs_parent_device ? strlen(sysfs_parent_device) : 0);
}

O que eu acho instrutivo nesse bug é a combinação que o produziu. Nenhuma das duas partes está errada sozinha. A biblioteca aberta avançou e refatorou um wrapper antigo assumindo que ninguém mais passaria NULL. O daemon fechado continuou usando a chamada antiga do jeito antigo, porque ninguém o recompilou. O ponto de encontro entre um software que evolui e um que ficou parado é onde o segfault mora. E a correção precisa ir na parte que evolui, porque é a única que se pode compilar.

O install.sh faz exatamente isso: extrai o código-fonte do EVDI que o próprio fabricante empacotou, aplica o patch, compila a libevdi, guarda a original como libevdi.so.orig, e instala a corrigida no lugar. O daemon fechado continua idêntico. Ele só passa a chamar uma biblioteca que não morre com o argumento que ele sempre mandou.

Causa dois: a tela cinza é uma corrida de GPUs

O terceiro sintoma é o mais assustador, porque impede o uso do computador, e tem a causa mais sutil. O instalador do fabricante cria /etc/modules-load.d/evdi.conf, que força o carregamento do módulo evdi em todo boot, com quatro placas virtuais. Essas quatro placas aparecem para o sistema durante a inicialização, no mesmo instante em que o driver da GPU real, o amdgpu, está subindo. O compositor do GNOME em Wayland, o mutter, precisa escolher uma GPU primária nessa confusão, e às vezes escolhe errado: pega o framebuffer de boot do EFI, o simpledrm, tenta renderizar nele, falha com gbm_surface_lock_front_buffer failed, e o que aparece na tela é cinza.

A pista era que o problema era intermitente e dependia do tempo de boot: condição de corrida clássica. A correção não é um patch; é uma decisão de projeto: o módulo não precisa existir no boot. Ele só é útil quando há um monitor USB conectado. Então o arquivo de carregamento forçado é removido (movido para dentro da pasta do fabricante, para poder ser restaurado), e o serviço do daemon passa a ser iniciado por uma regra udev no momento em que o adaptador é conectado. O evdi carrega sob demanda, depois que o compositor já escolheu a GPU certa. Desconectou, o serviço para. O boot fica limpo porque não há nada para disputar.

O padrão por trás da correção

Coisas que só servem em certas condições não devem estar presentes fora delas. Parece óbvio escrito assim, e é a regra que o instalador do fabricante viola ao carregar o módulo sempre. A versão sob demanda não é só mais correta; ela elimina uma classe inteira de problemas, porque um módulo que não está carregado não participa de corrida nenhuma.

Quatro placas virtuais: a parte em que foi preciso dizer "o ganho é pequeno"

Depois das três correções, sobrou uma pergunta: por que quatro placas virtuais? O módulo evdi pré-cria um pool de dispositivos ao carregar, controlado pela opção initial_device_count. O padrão do projeto EVDI é zero. Os quatro vêm do fabricante. E a quantidade que você realmente precisa é uma por saída física do adaptador: um monitor ligado direto no HDMI do notebook não passa pelo EVDI e não consome nenhuma.

O repositório ganhou um script para ajustar isso, e a parte que eu mais prezo na documentação dele é a honestidade sobre o tamanho do ganho. Cada placa ociosa custa kilobytes, não megabytes, porque nenhum framebuffer é alocado até um monitor conectar de fato. Baixar de quatro para duas não vai aparecer no gráfico de memória. Os benefícios reais são outros: uma lista de monitores que significa o que diz, sem saídas "desconectadas" fantasmas que o X11 e as Configurações enumeram; menos nós DRM para o compositor errar na escolha da GPU primária, que é a classe de problema que motivou o projeto; e um padrão que reflete o hardware em vez de um chute do fabricante.

E por que o fabricante escolheu quatro? O próprio EVDI documenta a opção como um workaround para o X.Org, que monta a lista de GPUs uma vez na inicialização e pode travar se um dispositivo aparece depois. Pré-criar um pool evita isso. No Wayland a restrição não existe. É um exemplo limpo de configuração que fazia sentido num contexto e foi copiada para outro sem que ninguém revisitasse o motivo.

Um detalhe que torna o ajuste seguro de experimentar: se o pool for pequeno demais, a libevdi cria uma placa sob demanda escrevendo em /sys/devices/evdi/add. Errar para baixo não deixa ninguém sem tela.

A parte mais trabalhosa: fazer tudo ter volta

Olhando as três correções, o código que resolve o problema é pequeno: uma linha de C, um arquivo movido, um tarball trocado. O que ocupou a maior parte do trabalho foi outra coisa: garantir que cada passo possa ser testado sem risco e desfeito sem rastro. Quem roda esses scripts está num sistema que já quebrou uma vez. A última coisa de que precisa é de um script que quebra de outro jeito.

ComandoO que fazPrecisa de root
diagnose.shRelata distribuição, GPU, adaptador, dependências e estado da correção. Só lê.Não
install.sh --build-onlyCompila a biblioteca corrigida num diretório temporário e confere. Não instala nada.Não
install.sh --dry-runImprime cada ação que faria. Não muda nada.Não
install.shAplica a correção, com backup da biblioteca e da configuração originais.Sim
uninstall.shRestaura a biblioteca e a configuração do fabricante.Sim

Três camadas de verificação antes de qualquer mudança, backups feitos uma única vez (um segundo install.sh não sobrescreve o .orig com uma versão já corrigida), e um caminho de volta documentado, inclusive com o aviso de que restaurar a configuração de boot do fabricante pode trazer a tela cinza de volta, e o que fazer se trouxer. O guia de contribuição pede o mesmo de quem quiser mandar código: scripts idempotentes, reversíveis, validados nos modos de leitura.

O que essa solução não resolve, dito de frente

O que eu levo desse projeto para qualquer outro

Primeiro, separar sintomas antes de procurar causa: três sintomas, três causas, e tentar um "fix" único teria falhado nos três. Segundo, achar a camada que se pode mudar: a aberta, compilável, com licença que permite. Terceiro, preferir remover a presença ao invés de consertar a interação: o módulo sob demanda elimina a corrida em vez de tentar vencê-la. E quarto, gastar o tempo que for preciso para que cada passo tenha dry-run e tenha volta, porque quem vai rodar aquilo já está com o sistema quebrado.

Por que isso está no GitHub e não só na minha máquina

Depois de uma noite de trabalho, o adaptador funciona por completo no Ubuntu 24.04 com GNOME e Wayland. Eu poderia ter parado aí. Em vez disso, coloquei tudo aberto, documentado em dois idiomas, com os scripts de diagnóstico e de reversão, para que qualquer pessoa com o mesmo problema instale e resolva sem passar pelos três dias que eu passei. É o que o open source tem de mais bonito: um problema que trava uma pessoa vira uma solução que destrava várias. Colaboração não é sobre fazer sozinho. É sobre não deixar o próximo começar do zero.

Resumo da conversa