História de como criar uma máquina do tempo para um banco de dados e escrever acidentalmente uma exploração

Bom dia, Habr.

Você já se perguntou como alterar o tempo dentro do banco de dados? Fácil? Bem, em alguns casos, sim, é fácil - o comando linux date e a coisa estão no chapéu. E se você precisar alterar o horário apenas dentro de uma instância do banco de dados, se houver vários deles no servidor? E para um único processo de banco de dados? E? É isso aí, meu amigo, esse é o ponto.Alguém dirá que esse é outro sur, não relacionado à realidade, que é periodicamente apresentado em Habré. Mas não, a tarefa é bastante real e é ditada pela necessidade de produção - teste de código. Embora eu concorde, o caso de teste pode ser bastante exótico - verifique como o código se comporta para uma determinada data no futuro. Neste artigo, examinarei em detalhes como essa tarefa foi resolvida e, ao mesmo tempo, capturamos um pouco o processo de organização de teste e desenvolvimento para a base do Oracle. Antes de uma longa leitura, fique à vontade e peça um gato.

fundo


Vamos começar com uma breve introdução para mostrar por que isso é necessário. Como já anunciado, escrevemos testes ao implementar edições no banco de dados. O sistema sob o qual esses testes são realizados foi desenvolvido no início (ou talvez um pouco antes do início) dos zero, portanto toda a lógica de negócios está dentro do banco de dados e escrita na forma de procedimentos armazenados na linguagem pl / sql. E sim, isso nos traz dor e sofrimento. Mas isso é legado, e você tem que viver com ele. No código e no modelo tabular, é possível especificar como os parâmetros dentro do sistema evoluem ao longo do tempo, ou seja, definem a atividade a partir de qual data e em que data eles podem ser aplicados. O que ir longe - a recente mudança na taxa de IVA é um exemplo vívido disso. E para que essas alterações no sistema possam ser verificadas com antecedência,Se um banco de dados com essas alterações precisar ser transferido para uma determinada data no futuro, os parâmetros de código nas tabelas serão ativados no "momento atual". E devido às especificidades do sistema suportado, você não pode usar testes simulados que simplesmente alterariam o valor de retorno da data atual do sistema no idioma quando a sessão de teste iniciar.

Então, determinamos o porquê, então precisamos determinar como o objetivo é alcançado. Para fazer isso, farei uma pequena retrospectiva das opções para a criação de bancos de teste para desenvolvedores e como cada sessão de teste foi iniciada.

Idade da Pedra


Era uma vez, quando as árvores eram pequenas e os mainframes grandes, havia apenas um servidor para desenvolvimento e também estava realizando testes. E, em princípio, tudo isso foi suficiente para todos ( 640K é o suficiente para todos! )

Contras: para executar a tarefa de alterar o tempo, era necessário envolver muitos departamentos relacionados - administradores de sistema (o tempo mudou no servidor secundário a partir da raiz), administradores de DBMS (o banco de dados foi reiniciado), programadores ( era necessário notificar que uma mudança de horário ocorreria, porque parte do código parou de funcionar, por exemplo, os tokens da web emitidos anteriormente para chamar métodos api deixaram de ser válidos e isso pode ser uma surpresa), testadores (testando-se) ... Quando você retorna o tempo para o presente tudo foi repetido em ordem inversa.

Meia idade


Com o tempo, o número de desenvolvedores no departamento cresceu e, em algum momento, 1 servidor deixou de ser suficiente. Principalmente devido ao fato de que diferentes desenvolvedores desejam alterar o mesmo pacote pl / sql e realizar testes para ele (mesmo sem alterar o tempo). Mais e mais indignação foi ouvida: “Quanto tempo! Chega de tolerar isso! Fábricas para trabalhadores, terra para camponeses! Todo programador tem um banco de dados! ” No entanto, se você tiver alguns terabytes de banco de dados do produto e 50 a 100 desenvolvedores, honestamente, neste formulário, o requisito não é muito real. E ainda assim todos querem que a base de teste e desenvolvimento não fique muito atrás das vendas, tanto na estrutura quanto nos dados dentro das tabelas. Portanto, havia um servidor separado para teste, vamos chamá-lo de pré-produção. Foi construído a partir de 2 servidores idênticos,onde a venda foi feita para restaurar o banco de dados do RMAN bucks e levou cerca de 2-2,5 dias. Após a recuperação, o banco de dados fez o anonimato dos dados pessoais e outros dados importantes e a carga dos aplicativos de teste foi aplicada a esse servidor (assim como os próprios programadores sempre trabalharam com o servidor restaurado recentemente). O trabalho com o servidor necessário foi assegurado usando o recurso IP do cluster suportado pelo corosync (pacemaker). Enquanto todos estão trabalhando com o servidor ativo, no segundo nó, a recuperação do banco de dados é iniciada novamente e após 2-3 dias eles mudam de lugar novamente.O trabalho com o servidor necessário foi assegurado usando o recurso IP do cluster suportado pelo corosync (pacemaker). Enquanto todos estão trabalhando com o servidor ativo, no segundo nó, a recuperação do banco de dados é iniciada novamente e após 2-3 dias eles mudam de lugar novamente.O trabalho com o servidor necessário foi assegurado usando o recurso IP do cluster suportado pelo corosync (pacemaker). Enquanto todos estão trabalhando com o servidor ativo, no segundo nó, a recuperação do banco de dados é iniciada novamente e após 2-3 dias eles mudam de lugar novamente.

Das desvantagens óbvias: você precisa de 2 servidores e 2 vezes mais recursos (principalmente disco) que o prod.

Prós: operação e teste de alteração de horário - ele pode ser realizado no 2º servidor, no servidor principal. Os desenvolvedores vivem e realizam seus negócios. A mudança do servidor ocorre apenas quando o banco de dados está pronto e o tempo de inatividade do ambiente de teste é mínimo.

A era do progresso científico e tecnológico


Quando mudamos para o banco de dados 11g Release 2, lemos sobre uma tecnologia interessante que a Oracle fornece sob o nome CloneDB. A conclusão é que os backups do banco de dados do produto (há uma cópia diretamente em bits dos arquivos de dados do produto) são armazenados em um servidor especial, que publica esse conjunto de arquivos de dados via DNFS (NFS direto) em basicamente qualquer número de servidores, e você não precisa ter um no servidor o mesmo volume de discos, porque a abordagem Copy-On-Write é implementada: o banco de dados usa um compartilhamento de rede com arquivos de dados do servidor de backup para ler dados em tabelas e as alterações são gravadas nos arquivos de dados locais no próprio servidor de desenvolvimento. Periodicamente, “zerando os prazos” é feito para o servidor, para que os arquivos de dados locais não cresçam muito e o local não termine. Ao atualizar o servidor, os dados também são despersonalizados nas tabelas,nesse caso, todas as atualizações de tabela caem em arquivos de dados locais e essas tabelas são lidas no servidor local, todas as outras tabelas são lidas na rede.

Contras: ainda existem 2 servidores (para garantir atualizações tranquilas com tempo de inatividade mínimo para os consumidores), mas agora o volume de discos é bastante reduzido. Para armazenar dinheiro em uma bola nfs, você precisa de mais 1 servidor em tamanho + - como um produto, mas o tempo de execução da atualização em si é reduzido (especialmente ao usar dinheiro incremental). O trabalho em rede com uma bola nfs reduz visivelmente as operações de leitura de E / S. Para usar a tecnologia CloneDB, a base deve ser uma Enterprise Edition; no nosso caso, tivemos que executar o procedimento de atualização em bases de teste a cada vez. Felizmente, os bancos de dados de teste estão isentos das políticas de licenciamento da Oracle.

Prós: a operação para restaurar uma base de um bakup leva menos de 1 dia (não me lembro da hora exata).

Mudança de tempo: sem grandes mudanças. Embora, a essa altura, os scripts já tivessem sido feitos para alterar a hora no servidor e reiniciar o banco de dados para fazer isso sem atrair a atenção dos ordenados dos administradores.

Era da Nova História


Para economizar ainda mais espaço em disco e tornar a leitura de dados offline, decidimos implementar nossa versão CloneDB (com flashback e snapshots) usando um sistema de arquivos com compactação. Durante os testes preliminares, a escolha recaiu sobre o ZFS, embora não haja suporte oficial para ele no kernel Linux (citação do artigo) Para comparação, também analisamos o BTRFS (b-tree fs), que a Oracle está promovendo, mas a taxa de compactação foi menor com o mesmo consumo de CPU e RAM nos testes. Para habilitar o suporte ao ZFS no RHEL5, foi criado seu próprio kernel baseado no UEK (kernel empresarial inquebrável) e, em eixos e kernels mais novos, você pode simplesmente usar o kernel UEK pronto. A implementação dessa base de teste também é baseada no mecanismo COW, mas no nível dos instantâneos do sistema de arquivos. 2 dispositivos de disco são fornecidos ao servidor; em um, o pool zfs é criado, onde através do RMAN é feito um banco de dados adicional em espera da venda e, como usamos a compactação, a partição ocupa menos que a produção.
O sistema está instalado no segundo dispositivo de disco e o restante é necessário para o servidor e o próprio banco de dados funcionarem, por exemplo, partições para desfazer e temp. A qualquer momento, você pode fazer uma captura instantânea do pool zfs, que é aberto como um banco de dados separado. Criar um instantâneo leva alguns segundos. É Magica! E, em princípio, esses bancos de dados podem ser bastante inclinados, se apenas o servidor tiver RAM suficiente para todas as instâncias e o tamanho do conjunto zfs (para armazenar alterações nos arquivos de dados durante a despersonalização e durante o ciclo de vida do clone do banco de dados). O principal momento para atualizar a base de teste é a operação de despersonalização de dados, mas também cabe em 15 a 20 minutos. Há aceleração significativa.

Contras: no servidor, você não pode alterar a hora simplesmente traduzindo a hora do sistema, porque todas as instâncias de banco de dados em execução neste servidor caem nesse momento de uma só vez. Uma solução para esse problema foi encontrada e será descrita na seção apropriada. Olhando para o futuro, direi que permite alterar o horário em apenas uma instância do banco de dados (abordagem por alteração de horário por instância) sem afetar o restante no mesmo servidor. E o tempo no próprio servidor também não muda. Isso elimina a necessidade de um script raiz para alterar a hora no servidor. Também nesta fase, a automação de alteração de horário para instâncias via Jenkins CI é implementada e os usuários (equipes de desenvolvimento relativamente falando) que possuem seu estande recebem direitos sobre os trabalhos pelos quais eles mesmos podem alterar o horário, atualizar o estande para o estado atual com vendas, fazer instantâneos e restauração (reversão) da base para o instantâneo criado anteriormente.

Era da História Recente


Com o advento do Oracle 12c, uma nova tecnologia apareceu - bancos de dados conectáveis ​​e, como resultado, bancos de dados de contêiner (cdb). Com essa tecnologia, em uma instância física, vários bancos de dados "virtuais" podem ser criados, compartilhando uma área de memória comum da instância. Prós: você pode economizar memória para o servidor (e aumentar o desempenho geral do nosso banco de dados, porque toda a memória ocupada antes, por exemplo, 5 instâncias diferentes, pode ser compartilhada para todos os contêineres pdb implantados no cdb, e eles a usarão apenas quando realmente precisam, e não como na fase anterior, quando cada instância "bloqueava" a memória alocada para si e com baixa atividade de alguns dos clones, a memória não era usada com eficiência, ou seja, estava ociosa).Os arquivos de dados de pdb diferente ainda estão no pool zfs e, ao implantar clones, eles usam o mesmo mecanismo de snapshot do zfs. Nesse estágio, chegamos perto o suficiente da capacidade de fornecer a quase todos os desenvolvedores seu próprio banco de dados. Alterar o horário nesse estágio não requer uma reinicialização do banco de dados e funciona com muita precisão apenas nos processos que precisam de um horário; todos os outros usuários que trabalham com esse banco de dados não são afetados de forma alguma.

Menos: você não pode usar a abordagem de alteração de tempo por instância da fase anterior, porque temos uma instância agora. No entanto, uma solução para este caso foi encontrada. E foi precisamente isso que serviu de impulso para a redação deste artigo. Olhando para o futuro, direi que é uma mudança de tempo por abordagem de processo , ou seja. em cada processo do banco de dados, você pode definir seu horário exclusivo em geral.

Nesse caso, uma sessão de teste típica imediatamente após a conexão com o banco de dados define o horário certo no início de seu trabalho, realiza os testes e retorna o tempo no final. É necessário retornar o tempo por um motivo simples: alguns processos do banco de dados Oracle não terminam quando o cliente do banco de dados se desconecta do servidor, são processos chamados servidores compartilhados, que, diferentemente dos processos dedicados, são executados quando o servidor do banco de dados é iniciado e permanece quase indefinidamente (no ideal imagem do mundo). Se você deixar o horário alterado em um processo desse servidor, outra conexão que será atendida nesse processo receberá o horário errado.

Em nosso sistema, os servidores compartilhados são muito usados, porque até 11g, praticamente não havia solução adequada para o nosso sistema suportar carga alta (em 11g apareceu o DRCP - pool de conexão residente no banco de dados). E aqui está o porquê - no sub, há um limite no número total de processos do servidor que ele pode criar nos modos dedicado e compartilhado. Processos dedicados são gerados mais lentamente do que o banco de dados pode emitir um processo compartilhado já pronto do pool de processos compartilhados, o que significa que quando novas conexões estão chegando constantemente (especialmente se o processo faz outras operações lentas), o número total de processos aumentará. Quando o limite de sessões / processos é atingido, o banco de dados deixa de atender a novas conexões e ocorre um colapso.A transição para o uso de um conjunto de processos compartilhados nos permitiu reduzir o número de novos processos no servidor durante a conexão.

É aí que a revisão das tecnologias para a construção de bancos de dados de teste é concluída e podemos finalmente começar a implementar os algoritmos de mudança de horário para o próprio banco de dados.

A abordagem falsa por instância


Como alterar a hora dentro do banco de dados?

A primeira coisa que veio à mente foi criar em um esquema que contenha todo o código da lógica de negócios, sua própria função, que sobrepõe as funções de linguagem que funcionam com o tempo (sysdate, current_date etc.) e, sob certas condições, começa a fornecer outros valores, por exemplo, você pode defina valores através do contexto da sessão no início da execução do teste. Não deu certo, as funções do idioma interno não se sobrepuseram às do usuário.

Em seguida, foram testados os sistemas de virtualização leve (Vserver, OpenVZ) e a containerização via docker. Também não funciona, eles usam o mesmo kernel que o sistema host, o que significa que usam os mesmos valores de timer do sistema. Caindo de novo.

E aqui não tenho medo de resgatar esta palavra, uma grande invenção do mundo Linux - redefinição / interceptação de funções no estágio de carregamento dinâmico de objetos compartilhados. É conhecido por muitos como truques com LD_PRELOAD. Na variável de ambiente LD_PRELOAD, você pode especificar a biblioteca que será carregada antes de todas as outras que o processo precisa e, se essa biblioteca contiver caracteres com o mesmo nome que, por exemplo, na libc padrão, que será carregada posteriormente, a tabela de importação de símbolos do aplicativo parecerá uma função fornece nosso módulo de substituição. E é exatamente isso que a biblioteca de projetos libfaketime fazque começamos a usar para iniciar o banco de dados em um horário diferente, separadamente do sistema. A biblioteca perde chamadas relacionadas ao trabalho com o timer do sistema e à obtenção da hora e data do sistema. Para controlar quanto tempo se move em relação à data atual do servidor ou a que ponto o tempo deve passar dentro do processo - tudo é controlado por variáveis ​​de ambiente que devem ser configuradas juntamente com LD_PRELOAD. Para implementar a mudança de horário, implementamos um trabalho no servidor Jenkins, que entra no servidor de banco de dados e reinicia o DBMS com ou sem variáveis ​​de ambiente definidas para libfaketime.

Um algoritmo de exemplo para iniciar um banco de dados com um horário de substituição:

export LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so
export FAKETIME="+1d"
export FAKETIME_NO_CACHE=1

$ORACLE_HOME/bin/sqlplus @/home/oracle/scripts/restart_db.sql

E se você pensa que tudo funcionou imediatamente, está profundamente enganado. Porque, como se viu, valida as bibliotecas que são carregadas no processo quando o DBMS é iniciado. E no alertlog, ele começa a se ressentir da falsificação notada, enquanto a base não inicia. Agora, não me lembro exatamente como se livrar dele; existe algum parâmetro que pode desativar a execução de verificações de sanidade na inicialização.

A abordagem falsa por processo


A idéia geral de alterar o tempo apenas dentro de 1 processo permaneceu a mesma - use libfaketime. Iniciamos o banco de dados com uma biblioteca pré-carregada nele, mas definimos um deslocamento de tempo zero na inicialização, que é propagado para todos os processos do DBMS. E então, dentro da sessão de teste, defina a variável de ambiente apenas para esse processo. Pff, negócios alguma coisa.

No entanto, para aqueles que estão familiarizados com a linguagem pl / sql, toda a destruição dessa idéia é imediatamente clara. Porque o idioma é muito limitado e basicamente adequado para tarefas de alto nível. Nenhuma programação do sistema pode ser implementada lá. Embora algumas operações de baixo nível (por exemplo, trabalhando com uma rede, trabalhando com arquivos) estejam presentes na forma de pacotes dbms / utl do sistema pré-instalados. Durante todo o tempo em que trabalhei com a Oracle, fiz engenharia reversa de pacotes pré-instalados várias vezes, o código de alguns deles fica oculto aos olhos de estranhos (eles são chamados de empacotados). Se você é proibido de assistir a algo, então a tentação de descobrir como ele é organizado no interior só aumenta. Mas muitas vezes, mesmo após o anvrapper, nem sempre há algo a ver, porque as funções desses pacotes são implementadas como interface c para as bibliotecas do disco.
No total, abordamos um candidato à implementação - tecnologia com procedimentos externos .
A biblioteca projetada de uma maneira especial pode exportar métodos, que o banco de dados Oracle pode chamar via pl / sql. Parece promissor. Somente quando conheci isso nos cursos avançados de plsql, lembrei-me muito remotamente de como cozinhá-lo. E isso significa que é necessário ler a documentação. Eu li - e imediatamente fiquei deprimido. Como o carregamento de uma biblioteca personalizada desse tipo passa por um processo de agente separado por meio de um ouvinte de banco de dados, e a comunicação com esse agente passa por dlink. Portanto, nossa ideia era definir uma variável de ambiente dentro do próprio processo do banco de dados. E tudo isso é feito por razões de segurança.

Uma imagem da documentação que mostra como funciona:



O tipo da biblioteca so / dll não é tão importante, mas por algum motivo a imagem é apenas para Windows.

Talvez alguém tenha notado aqui mais uma oportunidade em potencial. Sim, sim, isso é Java. O Oracle permite que você escreva o código do procedimento armazenado não apenas no plsql, mas também no java, que, no entanto, são exportados da mesma maneira que os métodos do plsql. Periodicamente, eu fazia isso, então não deveria haver um problema com isso. Mas então outra armadilha foi escondida. Java trabalha com uma cópia do ambiente e permite obter apenas as variáveis ​​de ambiente que o processo da JVM possuía na inicialização. A JVM interna herda as variáveis ​​de ambiente do processo do banco de dados, mas é tudo. Vi dicas na Internet sobre como alterar o mapa somente leitura através da reflexão, mas qual é o objetivo, porque ainda é apenas uma cópia. Ou seja, a mulher ficou novamente sem nada.

No entanto, Java não é apenas um item valioso. Usando-o, você pode gerar processos de dentro de um processo de banco de dados. Embora todas as operações não seguras devam ser resolvidas separadamente por meio do mecanismo de concessão de java, que é feito usando o pacote dbms_java. De dentro do código plsql, é possível obter o pid do processo atual do servidor no qual o código está sendo executado, usando as visualizações do sistema v $ session e v $ process. Além disso, podemos gerar algum processo filho da nossa sessão para fazer algo com esse pid. Para começar, deduzi todas as variáveis ​​de ambiente que estão dentro do processo do banco de dados (para testar a hipótese)

#!/bin/sh

pid=$1

awk 'BEGIN {RS="\0"; ORS="\n"} $0' "/proc/$pid/environ"

Bem deduzido, e depois o que. Ainda é impossível alterar as variáveis ​​no arquivo do ambiente, esses são os dados que foram transferidos para o processo quando ele foi iniciado e eles são somente leitura.

Pesquisei na Internet no stackoverflow "Como alterar uma variável de ambiente em outro processo". A maioria das respostas era que era impossível, mas havia uma resposta que descrevia essa oportunidade como um hack abaixo do padrão e sujo. E essa resposta foi Albert Einstein gdb. O depurador pode se conectar a qualquer processo que conhece seu pid e executar qualquer função / procedimento nele existente como um símbolo exportado publicamente, por exemplo, de alguma biblioteca. Na libc, existem funções para trabalhar com variáveis ​​de ambiente, e a libc é carregada em qualquer processo do banco de dados Oracle (e praticamente em qualquer programa no linux).

É assim que a variável de ambiente é definida em um processo externo (você precisa chamá-la do root por causa do ptrace usado):

#!/bin/sh

pid=$1
env_name=$2
env_val="$3"

out=`gdb -q -batch -ex "attach $pid" -ex 'call (int) setenv("'$env_name'", "'"$env_val"'", 1)' -ex "detach" 2>&1`


Além disso, ver as variáveis ​​de ambiente dentro do processo gdb também é adequado. Como mencionado anteriormente, o arquivo ambiente de / proc / pid / mostra apenas as variáveis ​​que existiam no início do processo. E se o processo criou algo no decorrer de seu trabalho, isso só pode ser visto através do depurador:
#!/bin/sh

pid=$1
var_name=$2

var_value=`gdb -q -batch -ex "attach $pid" -ex 'call (char*) getenv("'$var_name'")' -ex 'detach' | egrep '^\$1 ='`

if [ "$var_value" == '$1 = 0x0' ]
then
  # variable empty or does not exist
  echo -n
else
  # gdb returns $1 = hex_value "string value"
  var_hex=`echo "$var_value" | awk '{print $3}'`
  var_value=`echo "$var_value" | sed -r -e 's/^\$1 = '$var_hex' //;s/^"//;s/"$//'`
  
  echo -n "$var_value"
fi


Portanto, a solução já está no nosso bolso - através do java, criamos o processo de depuração, que vai para o processo que a gerou e define a variável de ambiente desejada para ela e depois termina (o Moor fez seu trabalho - o Moor pode sair). Mas havia um sentimento de que era algum tipo de muleta. Eu queria algo mais elegante. De alguma forma, seria o mesmo forçar o próprio processo do banco de dados a definir variáveis ​​de ambiente sem agressão externa.

Um ovo em um pato, um pato em uma lebre ...


E então alguém vem em socorro, sim, você adivinhou certo, novamente Java, a saber, JNI (java native interface). A JNI permite chamar métodos C nativos dentro da JVM. O código é emitido de uma maneira especial na forma de um objeto compartilhado da biblioteca, que a JVM carrega, enquanto os métodos que estavam na biblioteca mapeiam para os métodos java dentro da classe declarados com o modificador nativo.

Bem, ok, estamos escrevendo uma aula (na verdade, isso é apenas uma peça de trabalho):

public class Posix {

    private static native int setenv(String key, String value, boolean overwrite);

    private static native String getenv(String key);
    
    public static void stub() 
    {
        
    }
}

Depois disso, compile-o e obtenha o arquivo h gerado da futura biblioteca:

#  
javac Posix.java

#   Posix.h        JNI
javah Posix

Após receber o arquivo de cabeçalho, escrevemos o corpo para cada método:

#include <stdlib.h>
#include "Posix.h"

JNIEXPORT jint JNICALL Java_Posix_setenv(JNIEnv *env, jclass cls, jstring key, jstring value, jboolean overwrite)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = (char *) (*env)->GetStringUTFChars(env, value, NULL);

    int err = setenv(k, v, overwrite);

    (*env)->ReleaseStringUTFChars(env, key, k);
    (*env)->ReleaseStringUTFChars(env, value, v);

    return err;
}

JNIEXPORT jstring JNICALL Java_Posix_getenv(JNIEnv *env, jclass cls, jstring key)
{
    char* k = (char *) (*env)->GetStringUTFChars(env, key, NULL);
    char* v = getenv(k);

    return (*env)->NewStringUTF(env, v);
}

e compile a biblioteca

gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC Posix.c -shared -o libPosix.so -Wl,-soname -Wl,--no-whole-archive

strip libPosix.so

Para que o Java carregue a biblioteca nativa, ela deve ser encontrada pelo sistema ld de acordo com todas as regras do Linux. Além disso, o Java possui um conjunto de propriedades que contêm os caminhos nos quais as pesquisas da biblioteca ocorrem. A maneira mais fácil de trabalhar dentro do Oracle é colocar nossa biblioteca em $ ORACLE_HOME / lib.

E depois que criamos a biblioteca, precisamos compilar a classe dentro do banco de dados e publicá-la como um pacote plsql. Existem 2 opções para criar classes Java dentro do banco de dados:

  • carregar arquivo de classe binário via utilitário loadjava
  • compilar código de classe da fonte usando o sqlplus

Usaremos o segundo método, embora eles sejam basicamente iguais. Para o primeiro caso, foi necessário escrever imediatamente todo o código da classe no estágio 1, quando recebemos uma classe de stub para o arquivo h.

Para criar uma classe em subd, é usada uma sintaxe especial:

CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "Posix" AS
...
...
/

Quando a classe é criada, ela precisa ser publicada como métodos plsql, e aqui novamente a sintaxe especial:

procedure set_env(var_name varchar2, var_value varchar2)
is
language java name 'Posix.set_env(java.lang.String, java.lang.String)';

Quando você tenta chamar métodos potencialmente inseguros dentro de Java, é executada uma execução que diz que nenhuma concessão de java foi emitida para o usuário. Carregar métodos nativos é outra operação insegura, porque injetamos código estranho diretamente no processo do banco de dados (a mesma exploração anunciada no cabeçalho).

Mas, como o banco de dados é teste, concedemos uma concessão sem qualquer preocupação de conexão com o sys:

begin
dbms_java.grant_permission( 'SYSTEM', 'SYS:java.lang.RuntimePermission', 'loadLibrary.Posix', '');
commit;
end;
/

O nome de usuário do sistema é aquele em que compilei o código java e o pacote plsql wrapper.
É importante observar que, ao carregar uma biblioteca através de uma chamada para System.loadLibrary, omitimos o prefixo da lib e a extensão so (conforme descrito na documentação) e não passamos nenhum caminho para onde procurar. Existe um método System.load semelhante que só pode carregar uma biblioteca usando um caminho absoluto.

E então 2 surpresa desagradável nos espera - eu cheguei na próxima toca de coelho da Oracle. Ao emitir uma concessão, ocorre um erro com uma mensagem bastante nebulosa:

ORA-29532: Java call terminated by uncaught Java exception: java.lang.SecurityException: policy table update

O problema é pesquisado na Internet e leva ao My Oracle Support (também conhecido como Metalink). Porque De acordo com as regras da Oracle, a publicação de artigos de um metalink não é permitida em fontes abertas, mencionarei apenas o número do documento - 259471.1 (aqueles que têm acesso podem ler por conta própria).

A essência do problema é que a Oracle não permitirá apenas o carregamento de códigos de terceiros suspeitos em nosso processo. O que é lógico.

Mas como a base é testada e confiamos em nosso código, permitimos o download sem medos especiais.
Fuh, desventuras acabaram.

Está vivo, vivo


Respirando fundo, decidi tentar dar vida ao meu Frankenstein.
Iniciamos o banco de dados com o libfaketime pré-carregado e o deslocamento 0.
Conecte-se ao banco de dados e faça uma chamada para o código que simplesmente exibe o tempo antes e depois de alterar a variável de ambiente:

begin
dbms_output.enable(100000);
dbms_java.set_output(100000);
dbms_output.put_line('old time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
system.posix.set_env('FAKETIME','+1d');
dbms_output.put_line('new time: '||to_char(sysdate, 'dd.mm.yyyy hh24:mi:ss'));
end;
/


Funciona, droga! Honestamente, eu estava esperando mais algumas surpresas, como os erros do ORA-600. No entanto, o alerta tinha o número inteiro e o código continuava funcionando.
É importante observar que, se a conexão com o banco de dados for feita como dedicada, depois que a conexão for concluída, o processo será destruído e não haverá rastreamento. Mas se usarmos conexões compartilhadas, nesse caso, um processo pronto é alocado no pool de servidores, alteraremos o tempo através de variáveis ​​de ambiente e, quando desconectado, permanecerá alterado dentro do processo. E quando outra sessão do banco de dados cair no mesmo processo do servidor, ela receberá a hora errada para sua considerável surpresa. Portanto, no final da sessão de teste, é melhor sempre retornar o tempo para o deslocamento zero.

Conclusão


Espero que a história tenha sido interessante (e talvez até útil para alguém).

Os códigos-fonte estão todos disponíveis no Github .

A documentação da libfaketime também .

Como você faz os testes? E como você cria bancos de dados de desenvolvimento e teste em uma empresa?

Bônus para quem lê até o fim


All Articles