Os riscos da transição para time_t de 64 bits
(blogs.gentoo.org)- Em sistemas glibc de 32 bits, consultas da hora atual ou chamadas a
stat()podem falhar após 2038, então o Gentoo precisa de um caminho seguro para migrar para time_t de 64 bits - O time64 da glibc deve ser usado junto com Large File Support (LFS) e, em ambientes de 32 bits, coexistem a ABI antiga, a ABI LFS e a ABI LFS+time64
- Quando
time_taparece em APIs, estruturas ou argumentos de funções, a mudança na largura do tipo leva a uma quebra de ABI, criando mau funcionamento em tempo de execução e riscos de segurança ao misturar binários time32 e time64 - Como distribuição baseada em código-fonte, o Gentoo pode ficar com um sistema parcialmente migrado durante a recompilação de
@world, por causa de falhas ou dependências circulares - Após a correção de 2024-09-30, ficou claro que apenas mudar o libdir não basta, e uma marcação time64 envolvendo o carregador dinâmico e várias toolchains de linguagens continua sendo a principal restrição
O problema de 2038 e o escopo da migração para time64
- Aplicações de 32 bits que usam
time_tde 32 bits podem receber um erro-1em vez da hora atual em 2038, ou podem não conseguir executarstat()em arquivos - A direção básica da transição é mudar
time_tpara um tipo de 64 bits- O musl já fez a transição
- A glibc oferece suporte a isso como opção
- Algumas distribuições, como o Debian, já fizeram a transição
- Em uma distribuição baseada em código-fonte como o Gentoo, o próprio usuário recompila o sistema, então é preciso reduzir o tempo em que os pacotes permanecem em estados de ABI diferentes
- O risco central é que alterar a largura de
time_tquebra a ABI- Se uma API de biblioteca contém
time_t, todo código linkado a essa biblioteca deve usar a mesma largura de tipo - Migrar apenas uma parte para time64 não é seguro
- Se uma API de biblioteca contém
LFS e as três sub-ABIs de 32 bits
- Arquiteturas de 32 bits já tinham, há tempos, um problema de largura de tipos relacionados a arquivos
off_té usado para offsets de arquivosino_té usado para números de inode- Como originalmente tinham largura de 32 bits, arquivos maiores que 2 GiB ou números de inode fora do intervalo de 32 bits causavam problemas
- Para resolver isso, foi introduzido o Large File Support (LFS)
- Ele muda
off_teino_tpara variantes de 64 bits - Na glibc, ainda hoje isso é opcional
- Muitos pacotes ativaram LFS no upstream e trataram a quebra de ABI, mas o problema não foi completamente resolvido
- Ele muda
- O suporte time64 da glibc exige o uso de LFS, formando uma estrutura que resolve juntos os problemas de tamanho de arquivo e de tempo
- Sistemas de 32 bits têm três sub-ABIs
- ABI antiga: tipos de 32 bits
- LFS:
off_tde 64 bits,ino_tde 64 bits,time_tde 32 bits - time64: LFS +
time_tde 64 bits
- Uma única build da glibc é compatível com as três variantes, mas bibliotecas que usam esses tipos em suas APIs não podem misturar as três variantes entre si
Como a mudança de ABI quebra na prática
- Quando
time_tmuda de 32 para 64 bits, o layout das estruturas muda- Em uma estrutura de exemplo com
int a,time_t b,int cnessa ordem, o offset decdifere entretime_tde 32 bits etime_tde 64 bits - Misturar binários time32 e time64 pode fazer com que campos errados sejam lidos ou escritos, e até permitir acessos fora dos limites
- Em uma estrutura de exemplo com
- O tamanho de
struct stattambém varia conforme a ABI- Valor padrão da glibc em x86 de 32 bits: 88 bytes
- LFS: 96 bytes
- LFS + time64: 108 bytes
- Mesmo sem usar estruturas, há problemas em argumentos de funções
- No x86, argumentos de funções são passados pela pilha
- Se um dos argumentos for
time_t, a posição na pilha dos argumentos seguintes muda
- Em um experimento de exemplo, valores são corrompidos quando um programa time32 é linkado a uma biblioteca recompilada como time64
- A saída original é
a = 1, um valor de tempo correto,c = 3 - Se apenas a biblioteca for recompilada com
-D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64,becpassam a ser interpretados incorretamente
- A saída original é
- Hoje não há uma proteção prática que impeça essa mistura de ABIs, o que pode gerar quebras em tempo de execução e problemas de segurança
Por que a transição no Gentoo é mais difícil
- Distribuições binárias recompilam todos os pacotes e então o usuário faz o upgrade em uma etapa relativamente atômica
- Pode haver problemas com repositórios de terceiros ou programas compilados localmente, mas o processo como um todo é relativamente seguro
- O Gentoo precisa mudar a ABI no próprio sistema enquanto recompila
@world- ABIs incompatíveis podem se misturar entre a recompilação de dois pacotes
- Falhas em algumas recompilações podem deixar o sistema em um estado parcialmente migrado
- Por causa de dependências circulares, recompilar um pacote dependente pode quebrar ferramentas de build e tornar impossível continuar a recompilação
Mitigações em avaliação
- Há três direções em discussão
- Alterar
CHOST, a tupla de plataforma, para distinguir a nova ABI da ABI de 32 bits existente - Alterar o libdir da nova ABI para instalar bibliotecas recompiladas separadamente das bibliotecas antigas
- Introduzir uma distinção de ABI em nível binário para impedir que binários de sub-ABIs diferentes sejam linkados
- Alterar
- Os três métodos podem ser implementados de forma um tanto independente, mas alguns podem depender uns dos outros
- As strings de exemplo no texto podem não ser as strings reais da solução final
Distinguindo a ABI com CHOST
- A tupla de plataforma identifica a plataforma-alvo da toolchain e, no Gentoo, também é usada para distinguir ABIs de forma única para suporte a multilib
- A tupla é composta por quatro partes: arquitetura, vendor, sistema operacional e libc
- Ex.:
i386-pc-linux-gnu - Ex.:
i686-pc-linux-gnu - Ex.:
i686-unknown-linux-gnu
- Ex.:
- Ao introduzir uma nova ABI, já se usou a mudança do campo vendor ou o acréscimo de uma marcação de ABI ao campo libc
- No ABI ARM hardfloat, no passado foram usadas formas como
armv7a-hardfloat-linux-gnueabiearmv7a-unknown-linux-gnueabihf
- No ABI ARM hardfloat, no passado foram usadas formas como
- Há opções semelhantes para a ABI time64
i686-gentoo_t64-linux-gnui686-pc-linux-gnut64armv7a-gentoo_t64-linux-gnueabihfarmv7a-unknown-linux-gnueabihft64
- A mudança da tupla parece não exigir muitos patches
- A GNU toolchain e o GNU build system ignoram o conteúdo após
gnuno campo libc - O Clang precisa de um patch para selecionar automaticamente a ABI correta conforme a tupla
- A GNU toolchain e o GNU build system ignoram o conteúdo após
Mudança de libdir e preserved-libs
- libdir é o nome padrão do diretório de instalação de bibliotecas
- O padrão comum é
lib - Em arquiteturas correspondentes de 64 bits, convencionou-se usar frequentemente
lib64 - A ABI x32 do x86 usa
libx32, e a ABI n32 do MIPS usalib32
- O padrão comum é
- Para a ABI de 32 bits com time64, está em avaliação mudar o libdir de
libpara algo comolibt64 - Um libdir separado ajuda a reduzir a mistura de ABIs durante a transição
- Reduz o risco de um executável time64 ser linkado acidentalmente a uma biblioteca time32
- Permite preservar bibliotecas time32 com o recurso preserved-libs do Portage
- Opcionalmente, é possível oferecer perfis multilib time32 + time64 para manter compatibilidade com aplicações time32 pré-compiladas antigas
- Com preserved-libs, executáveis existentes continuam usando bibliotecas time32 até serem recompilados, enquanto bibliotecas recompiladas como time64 são instaladas no novo libdir
- A mudança de libdir exige patches na toolchain
- A glibc pode receber tratamento especial, porque o mesmo conjunto de bibliotecas é válido para várias sub-ABIs
- Pode ser necessário um
ld.soseparado para que o.interpde executáveis time64 aponte para old.sode time64
- Para suporte multilib adequado, também é necessária uma tupla de plataforma exclusiva para essa ABI
Marcação de incompatibilidade em nível binário
- Ao misturar binários de ABIs diferentes, em geral o linker ou o carregador dinâmico deve bloquear a operação
- Se você linkar um programa de 64 bits a uma biblioteca de 32 bits, o linker rejeita com
file in wrong format - O carregador dinâmico também rejeita com erros como
wrong ELF class: ELFCLASS32
- Se você linkar um programa de 64 bits a uma biblioteca de 32 bits, o linker rejeita com
- A distinção de ABIs existente usa vários mecanismos
ELFCLASS32eELFCLASS64- Machine identifiers como
EM_386eEM_X86_64 - O campo flags em ARM e MIPS
- Seções de atributos específicas de arquitetura
- time32 e time64 precisam de um mecanismo semelhante, mas isso não é simples
- Não parece haver um mecanismo geral reutilizável
- É necessária uma solução adequada para várias arquiteturas
- Adicionar uma nova seção ELF note e implementar suporte na toolchain parece um candidato realista
- Também é preciso considerar a possibilidade de o usuário desativar a proteção
- Se um software pré-compilado sem código-fonte não chama APIs que usam
time_t, ele pode continuar funcionando com as bibliotecas do sistema - Bloquear isso incondicionalmente pode ser uma solução pior que o problema
- Se um software pré-compilado sem código-fonte não chama APIs que usam
- Usar um libdir separado permite criar uma verificação de QA não fatal relativamente simples
- Distinguir executáveis time64 pelo
.interp - Verificar se programas time32 não carregam bibliotecas de
libt64 - Verificar se programas time64 não carregam bibliotecas diretamente de
lib
- Distinguir executáveis time64 pelo
Limites das aplicações pré-compiladas de 32 bits
- Além dos pacotes compilados a partir do código-fonte, há aplicações antigas disponibilizadas apenas como binários pré-compilados para x86 e PowerPC
- Isso vale especialmente para software proprietário e jogos antigos
- Elas sofrem tanto com problemas de compatibilidade com bibliotecas do sistema quanto com o próprio problema de 2038
- Para o problema de compatibilidade, a estrutura multilib existente oferece parte da solução
- No amd64, já existem um layout multilib e mecanismos para compilar várias versões de bibliotecas a fim de dar suporte a software de 32 bits
- Isso pode ser estendido distinguindo
abi_x86_32eabi_x86_t64 - É possível criar um novo perfil multilib x86 que ofereça suporte às duas ABIs
- A falha dos próprios programas de 32 bits após 2038 continua sendo mais difícil
- Uma opção é controlar o horário do sistema com faketime
- Outra opção é executar uma VM com o tempo ajustado para o passado
Correção de 2024-09-30: apenas libdir não basta
- A ideia inicial era otimista demais, e mudar apenas o libdir dificulta uma separação estável
- Como todos os libdirs são listados em
ld.so.conf, não dá para depender de caminhos de libdir codificados diretamente dentro dold.so- Prefixos customizados do LLVM já ajustam caminhos, e esse caso também exigiria tratamento especial
- Com isso, aumenta a probabilidade de a mudança de libdir depender de uma distinção de incompatibilidade binária
- Há três objetivos básicos que precisam ser cumpridos
- O carregador dinâmico deve distinguir binários time32 e time64
- Todo binário sem marcação time64 explícita deve ser considerado time32, para compatibilidade retroativa
- Todos os binários recém-compilados devem ter uma marcação time64 explícita, incluindo binários compilados em ambientes não C, como Rust
- Esse objetivo exige patches em várias toolchains de várias linguagens
- Seria difícil para o Gentoo manter isso apenas localmente, e é necessária cooperação de várias partes
- As arquiteturas-alvo em geral são consideradas legadas ou já não recebem suporte suficiente
- Outra questão é se outras toolchains vão gerar executáveis time64 corretos
- Se não forem ajustadas para seguir
_TIME_BITScomo programas em C, elas podem codificar uma largura específica detime_te quebrar
- Se não forem ajustadas para seguir
- Como todo binário sem marcação time64 explícita passaria a usar bibliotecas time32, o Gentoo não conseguiria executar executáveis de terceiros que não tenham sido corrigidos para receber a marcação correta
- Também se considera uma alternativa com objetivo mais baixo
- Injetar RPATH em todos os executáveis time64 para forçar diretamente o libdir time64
- Esse método não impede completamente o carregador dinâmico de usar bibliotecas time32, mas pode ajudar na transição sem grandes problemas de compatibilidade
- Por outro lado, também há a opção de não mudar permanentemente o libdir time64 e mudar temporariamente o libdir time32
- Injetar RPATH nos programas existentes e renomear o libdir
- Instalar as novas bibliotecas time64 no libdir existente
- Novos programas time64 não teriam um RPATH que força bibliotecas time32
- A vantagem é manter compatibilidade com outras distribuições que já fizeram a transição
Tarefas restantes
- Implementar as três soluções pode oferecer um caminho de transição mais limpo e relativamente seguro para sistemas Gentoo de 32 bits que usam glibc
- Porém, essas soluções se aplicam principalmente a pacotes compilados a partir do código-fonte
- Aplicações pré-compiladas de 32 bits ainda ficam com o problema de 2038, mesmo que a compatibilidade de ABI seja mantida
- O design geral ainda é um rascunho, e pode continuar mudando conforme experimentos, discussões e envio de patches
1 comentários
Comentários do Hacker News
No Gentoo há algumas opções que o texto não aborda, e parecem ter ficado de fora porque, pelo desenho do sistema do Gentoo, a carga de trabalho seria grande
.sopara refletir também mudanças de ABI dos pacotes dependentes. Em geral, bibliotecas compartilhadas têm números de versão no nome do arquivo e na versão interna, comolibfoo.so.1.0.0, e o pacote rastreia suas próprias quebras de ABI. Para suportartime_tde 64 bits, seria preciso adicionar a cada.soum elemento de versão controlado pela ABI dependente. O resultado seria parecido com o “usar outro libdir” do texto, mas isso talvez fosse muito mais intrusivo, embora pudesse virar uma base reutilizável para futuras mudanças de ABIVocê agenda a compilação de vários pacotes novos e os compila em um sandbox; depois, as novas compilações podem ser feitas de modo que, via union, olhem primeiro para o sandbox e só depois façam fallback para o sistema. Quando tudo estiver compilado, basta empacotar os resultados e movê-los do sandbox para o sistema real. Isso transformaria toda a atualização do Gentoo em algo parecido com uma transação, o que traria grandes vantagens também em outros aspectos
/, então basta mudar oROOTÉ possível recompilar todo o
@systeme@worlde instalar no subdiretório indicado, depois sincronizando tudo de uma vez. Se possível, é melhor fazer isso a partir de uma sessão live; em teoria, também dá para fazer bind mount de/em um subdiretório do novo local de instalação, entrar com chroot e sincronizar com o/superior realhttps://devmanual.gentoo.org/ebuild-writing/variables/#root
A forma como o Mac OS X tratou
off_teino_tpode dar uma pista. As chamadas e estruturas antigas continuaram funcionando, e foram adicionadas novas chamadas e tipos com sufixo64; macros do pré-processador podiam escolher para onde o nome realmente apontava, embora fosse raro escrever isso diretamenteEm compensação, o OS e o SDK têm versionamento, e no build você pode especificar a versão mais antiga do sistema operacional na qual o binário precisa rodar. Os headers escolhiam automaticamente os macros adequados com base nisso, e anotações de APIs novas/obsoletas usavam o mesmo mecanismo para gerar weak linking ou avisos. No começo isso era feito no pré-processador, mas hoje o compilador entende com mais precisão o que a Apple chama de disponibilidade de API, então parece viável fazer algo parecido em outras plataformas
Mesmo rodando no OS v.B, a aplicação X, declarada para o OS v.B, pode não conseguir fazer link com a aplicação Y, declarada para o OS v.A. Na prática, essa abordagem é bem próxima do que quase todas as plataformas já fazem, e fazer diferente quebraria imediatamente a compatibilidade binária existente
off_t, nem vão ter nos headers uma chave que escolha de forma transparente o conjunto correto de funções conforme o tamanho de tipo desejado pelo programa clienteAinda assim, o texto enfatiza que
time_té um problema maior do queoff_t. Uma razão plausível é quetime_testá muito mais espalhado.off_té um tipo POSIX envolvido em relativamente poucas interfaces, enquantotime_tfaz parte da ISO C e aparece por toda parte. Além disso, muito código em C assume quetime_té um tipo inteiro com a mesma largura deint, algo menos comum no caso deoff_tNo Debian também foi muito doloroso. Alguns provavelmente chegaram ao burnout, e muita gente apontava para distribuições baseadas em código-fonte dizendo “lá deve ser facílimo”
/usrSempre que vejo gente sofrendo com esse tipo de problema, penso como foi realmente uma sorte termos forçado isso na época do porte inicial do amd64 no FreeBSD. Foi possível definir os tipos básicos da ABI e decidir olhar para o futuro em vez de para o passado
O amd64 tinha uma característica interessante que facilitou esse trabalho. Como argumentos de função de 32 bits eram convertidos automaticamente para 64 bits durante a chamada, mesmo passando um inteiro de tempo de 32 bits para uma função que esperava
time_tde 64 bits, na maior parte dos casos simplesmente funcionava durante o trabalho inicial da plataforma. Assim, os detalhes menores puderam ficar para depoisHavia outras plataformas de 64 bits na época, mas sem
time_tde 64 bits, e o FreeBSD/amd64 foi o primeiro daquela família por volta de 2003~2005. Pelo que lembro, o sparc64 também migrou paratime_tde 64 bitsO maior problema era que o tzcode não era seguro para 64 bits naquela época. O algoritmo de normalização de
struct tmcaía em condições degeneradas, tentando calcular iterativamente dia/mês/ano detime_t(2^62). Em vez de alterar bastante o tzcode, pelo que lembro foi tratado para falhar de forma aproximada antes de 1900 ou depois de 10000. É bem possível que isso já tenha sido corrigido no upstream há muito tempoDurante alguns anos, lidamos no estilo jogo de acertar toupeira com confusões entre tempo de 32/64 bits causadas por código de terceiros tratando
int/long/time_tde forma descuidada em estruturas de dados de arquivo ou de rede, mas no geral não foi um grande problema. Usartime_tde 64 bits desde o primeiro dia evitou a maior parte dos problemas, e fazer isso desde o início era fácil. O Linux perdeu uma grande oportunidade de fazer o mesmo ao iniciar o porte para amd64/x86_64Como observação adicional, na época não conseguimos concluir o
ino_tde 64 bits. Números de inode de 32 bits estavam expostos em muitos lugares, como estruturas on-disk do sistema de arquivos, estruturas de diretório do UFS etc. Naquele momento, quando o FreeBSD/amd64 ainda era uma plataforma de nível inferior, não havia um jeito realista de resolver isso desde o início sem abalar bastante outras arquiteturas tier-1. O trabalho foi feito duas vezes, mas no fim outra pessoa concluiu, corrigindo junto constantes que eram curtas demais, como o comprimento do caminho de mountpointtime_t,off_teino_tde 64 bits desde o início. O problema agora é migrar o Linux de 32 bits paratime_tde 64 bitsoff_t, que passou a ser de 64 bits desde o 2.0. Ainda restam vestígios do tamanho antigo nas versões de 32 bits do LinuxEntendo que essa parte de os argumentos de função de 32 bits serem convertidos automaticamente para 64 bits durante a chamada só funciona para argumentos sem sinal. Ao carregar em
%edi, a parte superior de%rdié zerada por isso. A especificação da ABI SysV para x86-64 não diz que todos os valores em registradores ou na pilha são estendidos para o valor completo de 64 bits, e até a observação sobre booleanos diz que só o byte inferior é significativo, sugerindo que essa é a regra geraltime_tdo i386 para 64 bits, é bem surpreendente. Fico curioso se outras arquiteturas de 32 bits, como Motorola 68000 ou sparc32, também migraram paratime_tde 64 bitsEm um grande sistema Unix antigo de 32 bits, para lidar com datas futuras, já substituí funções da libc com
time_tassinado de 32 bits por equivalentes comtime_tsem sinal de 32 bits. Isso rendeu mais 68 anos depois de 2038, e até lá eu já não estarei mais aquiA desvantagem é não poder representar datas anteriores ao epoch Unix de 1970, mas como era um sistema de agenda, isso não era problema. Se datas passadas forem importantes, também daria para deslocar o epoch algumas décadas ou reduzir a resolução temporal de 1 segundo para 2 segundos. Cada abordagem tem seus problemas sutis, então depende do caso de uso
Na página de manual original do BSD, a seção “Bugs” do
tunefstinha a piada famosa: “You can tune a file system, but you can't tune a fish.”, e segundo “Expert C Programming”, no código-fonte dessa página de manual havia este comentário ao lado da piada“Se você tirar isso, o daemon do UNIX seguirá seus passos a partir de agora até o momento em que
time_tder wrap around.”Quando essa frase foi escrita nos anos 70, 2038 evidentemente era um futuro inimaginavelmente distante
https://progforperf.github.io/Expert_C_Programming.pdf
A principal sensação que isso me deixou foi: respeito o esforço, mas como usuário eu só queria migrar para uma distribuição não baseada em código-fonte como o Debian e encerrar esse problema
mkfs.ext4ou o sistema de arquivos usado nas partições/e/usr, montar, extrair o stage3, entrar em chroot e executaremerge $all-my-packages-that-where-installed-before-mkfsEm vez de atualizar aos poucos, você pode instalar uma nova cópia do Gentoo
Se houver software proprietário de terceiros, ainda podem surgir problemas mesmo em sistemas binários. Também pode haver problemas em pacotes de primeira parte instalados separadamente em etapas independentes
Não sou especialista em C, mas achava que aliases de tipo como
off_ttinham sido introduzidos justamente para poderem ser trocados depois. Mas isso não parece funcionar de forma clara, então fico na dúvida se entendi erradooff_tnormalmente significa que você não precisa reescrever o código, mas precisa recompilar tudo que usa esse tipooff_t, fosse possível reconstruir@worldatomicamente, não haveria problema; mas distribuições baseadas em código-fonte não recompilam@worldde forma atômica, e sim os pacotes um por umAí podem surgir problemas como
libc.sousandooff_tde 64 bits enquanto ogccfoi compilado com base emoff_tde 32 bits, fazendo ogcctravar. Pacotes necessários para reconstruir@world, comobash,coreutils,makeebinutils, também podem quebrar, e nesse ponto você fica bloqueado. Por isso upgrades desse tipo exigem cuidadooff_tentra em uma struct, é usado em chamadas de função ou é incorporado a um protocolo, a abstração desaparece e o tamanho real passa a importarSe você misturar código antigo e novo ao carregar bibliotecas ou se comunicar por protocolo, os offsets começam a sair do lugar e os crashes começam a acontecer. No fim, a transição exige que todos separem seus programas em “legado” e “portado ou pelo menos revisado”, o que é extremamente doloroso
Mesmo ao trocar por um tipo maior com significado parecido, ainda pode quebrar. Um exemplo simples é padding em struct, e também há muitos usos em que ponteiros são convertidos para inteiros e depois de volta, então, se a representação interna mudar, a quebra é inevitável. Se isso é uma boa prática ou não é outra questão, mas está longe de ser raro. O ponto central é a compatibilidade de ABI
off_tde 32 bits com bibliotecas compiladas comoff_tde 64 bits, e o comportamento resultante pode ser extremamente imprevisívelNo exemplo da struct, foi dito que o offset de
cé 8 quandotime_té de 32 bits e 12 quando o tipo é de 64 bits, mas na verdade não deveria ser 16? Comobprecisa estar alinhado em 64 bits, deveria haver padding entreaeb. Na verdade isso até reforça ainda mais o argumento que o autor quer fazerVendo tudo isso, dá para dizer que a representação estranha de tempo do Windows — contar em 64 bits com unidades de 100 ns desde 00:00 GMT de 1º de janeiro de 1601 no calendário gregoriano — até tem uma pequena vantagem. A resolução também é excelente e vai continuar funcionando até a conquista da galáxia inteira