Récit de la façon de créer une machine à remonter le temps pour une base de données et d'écrire accidentellement un exploit

Bonjour, Habr.

Vous êtes-vous déjà demandé comment changer l'heure dans la base de données? Facile? Eh bien, dans certains cas, oui, c'est facile - la commande linux date et le truc sont dans le chapeau. Et si vous devez modifier l'heure uniquement à l'intérieur d'une instance de la base de données s'il y en a plusieurs sur le serveur? Et pour un seul processus de base de données? ET? Euh, c'est ça, mon ami, c'est tout le problème.Quelqu'un dira qu'il s'agit d'un autre sur, sans rapport avec la réalité, qui est périodiquement présenté sur Habré. Mais non, la tâche est bien réelle et est dictée par la nécessité de la production - les tests de code. Bien que je sois d'accord, le cas de test peut être assez exotique - vérifiez comment le code se comporte pour une certaine date à l'avenir. Dans cet article, je vais examiner en détail comment cette tâche a été résolue, et en même temps capturer un peu le processus d'organisation des tests et dev représente la base Oracle. Avant une longue lecture, installez-vous confortablement et demandez un chat.

Contexte


Commençons par une courte introduction pour montrer pourquoi cela est nécessaire. Comme déjà annoncé, nous écrivons des tests lors de l'implémentation des modifications dans la base de données. Le système sous lequel ces tests sont effectués a été développé au début (ou peut-être un peu avant le début) des zéro, donc toute la logique métier est à l'intérieur de la base de données et écrite sous la forme de procédures stockées dans le langage pl / sql. Et oui, cela nous fait souffrir et souffrir. Mais c'est un héritage, et vous devez vivre avec. Dans le code et le modèle tabulaire, il est possible de spécifier comment les paramètres à l'intérieur du système évoluent dans le temps, en d'autres termes, de définir l'activité à partir de quelle date et à quelle date ils peuvent être appliqués. Que faire? La récente modification du taux de TVA en est un exemple frappant. Et pour que ces changements dans le système puissent être vérifiés à l'avance,une base de données avec de tels changements doit être transférée à une certaine date dans le futur, les paramètres de code dans les tableaux deviendront actifs au "moment actuel". Et en raison des spécificités du système pris en charge, vous ne pouvez pas utiliser de faux tests qui modifieraient simplement la valeur de retour de la date actuelle du système dans la langue au démarrage de la session de test.

Nous avons donc déterminé pourquoi, puis nous devons déterminer comment l' objectif est atteint. Pour ce faire, je ferai une petite rétrospective des options de construction de bancs de test pour les développeurs et comment a commencé chaque session de test.

Ă‚ge de pierre


Il était une fois, lorsque les arbres étaient petits et les gros ordinateurs centraux, il n'y avait qu'un seul serveur à développer et il effectuait également des tests. Et en principe, tout cela était suffisant pour tout le monde ( 640K est suffisant pour tout le monde! )

Inconvénients: pour la tâche de changer l'heure, il fallait impliquer de nombreux départements liés - administrateurs système (faisant le changement d'heure sur le serveur subd à partir de la racine), administrateurs de SGBD (faisant le redémarrage de la base de données), programmeurs ( il était nécessaire de notifier qu'un changement d'heure se produirait, car une partie du code ne fonctionnait plus, par exemple, les jetons Web précédemment émis pour appeler les méthodes api avaient cessé d'être valides et cela pouvait surprendre), les testeurs (se testant eux-mêmes) ... lorsque vous revenez l'heure au présent tout a été répété dans l'ordre inverse.

Moyen Ă‚ge


Au fil du temps, le nombre de développeurs dans le département a augmenté et à un moment donné, le serveur a cessé d'être suffisant. Principalement dû au fait que différents développeurs souhaitent modifier le même package pl / sql et effectuer des tests pour celui-ci (même sans changer l'heure). De plus en plus d'indignation se fait entendre: «Combien de temps! Assez tolérant cela! Des usines aux ouvriers, des terres aux paysans! Chaque programmeur a une base de données! ” Cependant, si vous avez quelques téraoctets de base de données de produits et 50 à 100 développeurs, alors honnêtement sous cette forme, l'exigence n'est pas très réelle. Et pourtant, tout le monde veut que la base de test et de développement ne soit pas très en retard sur les ventes, à la fois dans la structure et dans les données à l'intérieur des tableaux. Il y avait donc un serveur séparé pour les tests, appelons-le pré-production. Il a été construit à partir de 2 serveurs identiques,où la vente a été faite pour restaurer la base de données à partir de dollars RMAN et cela a pris environ 2 à 2,5 jours. Après la récupération, la base de données a rendu l'anonymat des données personnelles et d'autres données importantes et la charge des applications de test a été appliquée à ce serveur (ainsi que les programmeurs eux-mêmes ont toujours travaillé avec le serveur récemment restauré). Le travail avec le serveur requis a été assuré à l'aide de la ressource ip de cluster prise en charge via corosync (pacemaker). Pendant que tout le monde travaille avec le serveur actif, sur le 2ème noeud, la récupération de la base de données recommence et après 2-3 jours, ils changent à nouveau de place.Le travail avec le serveur requis a été assuré à l'aide de la ressource ip de cluster prise en charge via corosync (pacemaker). Pendant que tout le monde travaille avec le serveur actif, sur le 2ème noeud, la récupération de la base de données recommence et après 2-3 jours, ils changent à nouveau de place.Le travail avec le serveur requis a été assuré à l'aide de la ressource ip de cluster prise en charge via corosync (pacemaker). Pendant que tout le monde travaille avec le serveur actif, sur le 2ème noeud, la récupération de la base de données recommence et après 2-3 jours, ils changent à nouveau de place.

Des inconvénients évidents: vous avez besoin de 2 serveurs et 2 fois plus de ressources (principalement disque) que prod.

Avantages: opération de changement d'heure et tests - elle peut être effectuée sur le 2ème serveur, sur le serveur principal à ce moment-là, les développeurs vivent et vaquent à leurs occupations. Le changement de serveur ne se produit que lorsque la base de données est prête et que les temps d'arrêt de l'environnement de test sont minimes.

L'ère du progrès scientifique et technologique


Lorsque nous sommes passés à la base de données 11g Release 2, nous avons découvert une technologie intéressante qu'Oracle fournit sous le nom de CloneDB. L'essentiel est que les sauvegardes de la base de données de produits (il existe une copie directement en bits des fichiers de données de produit) sont stockées sur un serveur spécial, qui publie ensuite cet ensemble de fichiers de données via DNFS (NFS direct) sur pratiquement n'importe quel nombre de serveurs, et vous n'avez pas besoin d'en avoir un sur le serveur le même volume de disques, car l'approche Copy-On-Write est implémentée: la base de données utilise un partage réseau avec les fichiers de données du serveur de sauvegarde pour lire les données dans les tableaux, et les modifications sont écrites dans des fichiers de données locaux sur le serveur de développement lui-même. Périodiquement, la «remise à zéro des délais» est effectuée pour le serveur afin que les fichiers de données locaux ne se développent pas beaucoup et que le lieu ne se termine pas. Lors de la mise à jour du serveur, les données sont également dépersonnalisées dans les tableaux,dans ce cas, toutes les mises à jour des tables tombent dans des fichiers de données locaux et ces tables sont lues à partir du serveur local, toutes les autres tables sont lues sur le réseau.

Inconvénients: il y a encore 2 serveurs (pour assurer des mises à jour fluides avec un temps d'arrêt minimal pour les consommateurs), mais maintenant le volume des disques est considérablement réduit. Pour stocker des dollars sur une balle nfs, vous avez besoin d'un serveur supplémentaire de taille + - en tant que prod, mais le temps d'exécution de la mise à jour lui-même est réduit (en particulier lors de l'utilisation de dollars incrémentiels). La mise en réseau avec une boule nfs ralentit sensiblement les opérations de lecture IO. Pour utiliser la technologie CloneDB, la base doit être une Enterprise Edition; dans notre cas, nous avons dû effectuer à chaque fois la procédure de mise à niveau sur les bases de test. Heureusement, les bases de données de test sont exemptées des politiques de licence Oracle.

Avantages: l'opération de restauration d'une base à partir d'un bakup prend moins d'un jour (je ne me souviens pas de l'heure exacte).

Changement d'heure: pas de changements majeurs. Bien que, à ce moment-là, des scripts aient déjà été créés pour modifier l'heure sur le serveur et redémarrer la base de données afin de le faire sans attirer l' attention des aides-soignants des administrateurs.

L'ère de la nouvelle histoire


Afin d'économiser encore plus d'espace disque et de rendre la lecture des données hors ligne, nous avons décidé d'implémenter notre version CloneDB (avec flashback et snapshots) en utilisant un système de fichiers avec compression. Lors des tests préliminaires, le choix s'est porté sur ZFS, bien qu'il n'y ait pas de support officiel pour celui-ci dans le noyau Linux (citation de l' article) À titre de comparaison, nous avons également examiné BTRFS (b-tree fs), dont Oracle fait la promotion, mais le taux de compression était inférieur avec la même consommation de CPU et de RAM dans les tests. Pour activer la prise en charge ZFS sur RHEL5, son propre noyau basé sur UEK (noyau d'entreprise incassable) a été construit, et sur les axes et noyaux plus récents, vous pouvez simplement utiliser le noyau UEK prêt à l'emploi. La mise en œuvre d'une telle base de test est également basée sur le mécanisme COW, mais au niveau des instantanés du système de fichiers. 2 périphériques de disque sont fournis au serveur, sur un, le pool zfs est créé, où via RMAN une base de données de secours supplémentaire est créée à partir de la vente, et puisque nous utilisons la compression, la partition prend moins de temps que la production.
Le système est installé sur le deuxième périphérique de disque et le reste est nécessaire au serveur et à la base de données elle-même pour fonctionner, par exemple, les partitions d'annulation et de temp. À tout moment, vous pouvez créer un instantané à partir du pool zfs, qui s'ouvre ensuite en tant que base de données distincte. La création d'un instantané prend quelques secondes. C'est magique! Et ces bases de données peuvent en principe être inclinées beaucoup, si seulement le serveur avait suffisamment de RAM pour toutes les instances et la taille du pool zfs lui-même (pour stocker les modifications dans les fichiers de données pendant la dépersonnalisation et pendant le cycle de vie du clone de base de données). Le principal moment de mise à jour de la base de test est le fonctionnement de la dépersonnalisation des données, mais il tient également en 15-20 minutes. Il y a une accélération importante.

Inconvénients: sur le serveur, vous ne pouvez pas changer l'heure simplement en traduisant l'heure du système, car alors toutes les instances de base de données exécutées sur ce serveur tomberont dans cette heure à la fois. Une solution à ce problème a été trouvée et sera décrite dans la section appropriée. Pour l'avenir, je dirai que cela vous permet de modifier l'heure à l'intérieur d'une seule instance de la base de données ( par approche de changement d'heure par instance) sans affecter le reste sur le même serveur. Et l'heure sur le serveur lui-même ne change pas non plus. Cela élimine le besoin d'un script racine pour changer l'heure sur le serveur. Toujours à ce stade, l'automatisation du changement d'heure pour les instances via Jenkins CI est implémentée et les utilisateurs (équipes de développement relativement parlant) qui possèdent leur stand ont le droit de travailler grâce auxquels ils peuvent eux-mêmes changer l'heure, mettre à jour le stand à l'état actuel avec les ventes, faire des instantanés et restauration (restauration) de la base à l'instantané précédemment créé.

L'ère de l'histoire récente


Avec l'avènement d'Oracle 12c, une nouvelle technologie est apparue - les bases de données enfichables et, par conséquent, les bases de données de conteneurs (cdb). Avec cette technologie, au sein d'une instance physique, plusieurs bases de données «virtuelles» peuvent être créées qui partagent une zone de mémoire commune de l'instance. Avantages: vous pouvez économiser de la mémoire pour le serveur (et augmenter les performances globales de notre base de données, car toute la mémoire qui était occupée auparavant, par exemple, 5 instances différentes, peut être partagée pour tous les conteneurs pdb déployés dans cdb, et ils ne l'utiliseront que quand ils en ont vraiment besoin, et non pas comme c'était le cas lors de la phase précédente, lorsque chaque instance "bloquait" la mémoire qui lui était allouée et avec une faible activité de certains des clones, la mémoire n'était pas utilisée efficacement, en d'autres termes, elle était inactive).Les fichiers de données de différents pdb se trouvent toujours dans le pool zfs, et lors du déploiement de clones, ils utilisent le même mécanisme de capture instantanée zfs. À ce stade, nous nous sommes rapprochés de la possibilité de donner à presque tous les développeurs leur propre base de données. La modification de l'heure à ce stade ne nécessite pas de redémarrage de la base de données et ne fonctionne de manière très précise que pour les processus qui nécessitent une modification de l'heure; tous les autres utilisateurs travaillant avec cette base de données ne sont en aucune façon affectés.

Moins: vous ne pouvez pas utiliser l'approche de changement de temps par instance de la phase précédente, car nous avons maintenant une instance. Cependant, une solution à ce cas a été trouvée. Et c'est précisément cela qui a donné l'impulsion à la rédaction de cet article. Pour l'avenir, je dirai qu'il s'agit d'un changement de temps par approche de processus , c'est-à-dire dans chaque processus de base de données, vous pouvez définir votre propre heure unique en général.

Dans ce cas, une session de test typique immédiatement après la connexion à la base de données définit le bon moment au début de son travail, effectue des tests et renvoie le temps à la fin. Le retour de l'heure est nécessaire pour une raison simple: certains processus de base de données Oracle ne se terminent pas lorsque le client de base de données se déconnecte du serveur, ce sont des processus serveur appelés serveurs partagés, qui, contrairement aux processus dédiés, s'exécutent lorsque le serveur de base de données démarre et vivent presque indéfiniment (dans l'idéal image du monde). Si vous laissez l'heure modifiée dans un tel processus de serveur, alors une autre connexion qui sera servie dans ce processus recevra l'heure incorrecte.

Dans notre système, les serveurs partagés sont beaucoup utilisés, car jusqu'à 11 g, il n'y avait pratiquement pas de solution adéquate pour que notre système résiste à une charge élevée (en 11 g, DRCP est apparu - regroupement de connexions résidentes de la base de données). Et voici pourquoi - dans sub, il y a une limite sur le nombre total de processus serveur qu'il peut créer en mode dédié et partagé. Les processus dédiés sont générés plus lentement que la base de données ne peut émettre un processus partagé déjà prêt à partir du pool de processus partagés, ce qui signifie que lorsque de nouvelles connexions arrivent constamment (en particulier si le processus effectue d'autres opérations lentes), le nombre total de processus augmente. Lorsque la limite de sessions / processus est atteinte, la base de données cesse de desservir de nouvelles connexions et un effondrement se produit.La transition vers l'utilisation d'un pool de processus partagés nous a permis de réduire le nombre de nouveaux processus sur le serveur lors de la connexion.

C’est là que l’examen des technologies de création de bases de données de test est terminé, et nous pouvons enfin commencer à implémenter les algorithmes de changement de temps pour la base de données elle-même.

La fausse approche par instance


Comment changer l'heure dans la base de données?

La première chose qui m'est venue à l'esprit était de créer dans un schéma qui contient tout le code logique métier, sa propre fonction, qui chevauche les fonctions de langage qui fonctionnent avec le temps (sysdate, current_date, etc.) et, sous certaines conditions, commence à donner d'autres valeurs, par exemple, vous pourriez définissez les valeurs via le contexte de la session au début de l'exécution du test. Cela n'a pas fonctionné, les fonctions de langage intégrées ne se chevauchaient pas avec celles de l'utilisateur.

Ensuite, des systèmes de virtualisation légers (Vserver, OpenVZ) et la conteneurisation via docker ont été testés. Cela ne fonctionne pas non plus, ils utilisent le même noyau que le système hôte, ce qui signifie qu'ils utilisent les mêmes valeurs de temporisation système. Tomber à nouveau.

Et ici, je n'ai pas peur de venir à la rescousse de ce mot, une brillante invention du monde Linux - redéfinition / interception de fonctions au stade du chargement dynamique d'objets partagés. Il est connu de beaucoup comme des astuces avec LD_PRELOAD. Dans la variable d'environnement LD_PRELOAD, vous pouvez spécifier la bibliothèque qui sera chargée avant toutes les autres dont le processus a besoin, et si cette bibliothèque contient des caractères avec le même nom que par exemple dans la libc standard, qui sera chargée plus tard, alors la table d'importation de symboles pour l'application ressemblera à une fonction fournit notre module de remplacement. Et c'est exactement ce que fait la bibliothèque de projet libfaketimeque nous avons commencé à utiliser afin de démarrer la base de données à un moment différent de celui du système. La bibliothèque manque des appels qui concernent l'utilisation du minuteur du système et l'obtention de l'heure et de la date du système. Pour contrôler combien de temps se déplace par rapport à la date actuelle du serveur ou à partir de quel moment le temps doit passer à l'intérieur du processus - tout est contrôlé par des variables d'environnement qui doivent être définies avec LD_PRELOAD. Pour implémenter le changement d'heure, nous avons implémenté un travail sur le serveur Jenkins, qui entre dans le serveur de base de données et redémarre le SGBD avec ou sans variables d'environnement définies pour libfaketime.

Un exemple d'algorithme pour démarrer une base de données avec un temps de substitution:

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

Et si vous pensez que tout a fonctionné tout de suite, vous vous trompez profondément. Car, comme il s'est avéré, valide les bibliothèques qui sont chargées dans le processus au démarrage du SGBD. Et dans le journal des alertes, il commence à ressentir la contrefaçon constatée, tandis que la base ne démarre pas. Maintenant, je ne me souviens pas exactement comment m'en débarrasser, il y a un paramètre qui peut désactiver l'exécution des vérifications d'intégrité au démarrage.

La fausse approche par processus


L'idée générale de changer l'heure uniquement en 1 processus est restée la même - utilisez libfaketime. Nous démarrons la base de données avec une bibliothèque préchargée, mais définissons un décalage de temps nul au démarrage, qui est ensuite propagé à tous les processus SGBD. Et puis, à l'intérieur de la session de test, définissez la variable d'environnement pour ce processus uniquement. Pff, quelque chose d'affaires.

Cependant, pour ceux qui sont familiers avec le langage pl / sql, tout le destin de cette idée est immédiatement clair. Parce que la langue est très limitée et convient essentiellement aux tâches de haut niveau. Aucune programmation système ne peut y être implémentée. Bien que certaines opérations de bas niveau (par exemple, travailler avec un réseau, travailler avec des fichiers) soient présentes sous la forme de packages dbms / utl système préinstallés. Pendant tout le temps que j'ai travaillé avec Oracle, j'ai fait plusieurs fois l'ingénierie inverse des packages préinstallés, le code de certains d'entre eux est caché aux yeux des étrangers (ils sont appelés wrappés). S'il vous est interdit de regarder quelque chose, la tentation de découvrir comment il est disposé à l'intérieur ne fait qu'augmenter. Mais souvent, même après anvrapper, il n'y a pas toujours quelque chose à voir, car les fonctions de ces packages sont implémentées en tant qu'interface c avec des so-bibliothèques sur disque.
Au total, nous avons approché un candidat pour la mise en œuvre - la technologie avec des procédures externes .
La bibliothèque conçue de manière spéciale peut exporter des méthodes que la base de données Oracle peut appeler via pl / sql. Semble prometteur. Ce n'est qu'une fois que j'ai rencontré cela dans des cours de plsql avancé, donc je me suis souvenu très à distance comment le cuisiner. Et cela signifie qu'il est nécessaire de lire la documentation. Je l'ai lu - et je suis immédiatement déprimé. Parce que le chargement d'une telle bibliothèque personnalisée se fait dans un processus d'agent distinct via un écouteur de base de données, et la communication avec cet agent passe par dlink. Notre idée a donc pleuré de définir une variable d'environnement dans le processus de base de données lui-même. Et tout cela est fait pour des raisons de sécurité.

Une image de la documentation qui montre comment cela fonctionne:



Le type de la bibliothèque so / dll n'est pas si important, mais pour une raison quelconque, l'image est uniquement pour Windows.

Peut-être que quelqu'un a remarqué ici une autre opportunité potentielle. Oui, oui, c'est Java. Oracle vous permet d'écrire du code de procédure stockée non seulement en plsql, mais aussi en java, qui sont néanmoins exportés de la même manière que les méthodes plsql. Périodiquement, je l'ai fait, donc cela ne devrait pas poser de problème. Mais un autre écueil s'est caché. Java fonctionne avec une copie de l'environnement et vous permet d'obtenir uniquement les variables d'environnement que le processus JVM avait au démarrage. La JVM intégrée hérite des variables d'environnement du processus de base de données, mais c'est tout. J'ai vu des conseils sur Internet pour changer la carte en lecture seule par réflexion, mais à quoi ça sert, car ce n'est encore qu'une copie. Autrement dit, la femme s'est retrouvée avec rien.

Cependant, Java n'est pas seulement une fourrure précieuse. En l'utilisant, vous pouvez générer des processus à partir d'un processus de base de données. Bien que toutes les opérations dangereuses doivent être résolues séparément via le mécanisme de subventions java, qui sont effectuées à l'aide du package dbms_java. Depuis l'intérieur du code plsql, vous pouvez obtenir le pid de processus du processus serveur actuel dans lequel le code s'exécute, en utilisant les vues système v $ session et v $ process. De plus, nous pouvons générer un processus enfant de notre session pour faire quelque chose avec ce pid. Pour commencer, j'ai simplement déduit toutes les variables d'environnement qui sont à l'intérieur du processus de base de données (pour tester l'hypothèse)

#!/bin/sh

pid=$1

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

Bien déduit, et puis quoi. Il est toujours impossible de modifier les variables dans le fichier environ, ce sont les données qui ont été transférées au processus lors de son démarrage et elles sont en lecture seule.

J'ai cherché sur Internet sur stackoverflow "Comment changer une variable d'environnement dans un autre processus." La plupart des réponses étaient que c'était impossible, mais il y avait une réponse qui décrivait cette opportunité comme un hack de mauvaise qualité et sale. Et cette réponse était Albert Einstein gdb. Le débogueur peut s'accrocher à n'importe quel processus connaissant son pid et exécuter n'importe quelle fonction / procédure qui y existe en tant que symbole exporté publiquement, par exemple, à partir d'une bibliothèque. Dans libc, il existe des fonctions pour travailler avec des variables d'environnement, et libc est chargé dans n'importe quel processus de la base de données Oracle (et pratiquement dans n'importe quel programme sous linux).

Voici comment la variable d'environnement est définie dans un processus étranger (vous devez l'appeler à partir de la racine en raison du ptrace utilisé):

#!/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`


De plus, pour voir les variables d'environnement à l'intérieur du processus gdb est également approprié. Comme mentionné précédemment, le fichier environ de / proc / pid / affiche uniquement les variables qui existaient au début du processus. Et si le processus a créé quelque chose au cours de son travail, cela ne peut être vu qu'à travers le débogueur:
#!/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


Donc, la solution est déjà dans notre poche - via java, nous générons le processus de débogage, qui va au processus qui l'a généré et définit la variable d'environnement souhaitée pour lui, puis se termine (le Maure a fait son travail - le Maure peut partir). Mais on avait l'impression que c'était une sorte de béquille. Je voulais quelque chose de plus élégant. Ce serait en quelque sorte la même chose pour forcer le processus de la base de données à définir des variables d'environnement sans assaut externe.

Un œuf dans un canard, un canard dans un lièvre ...


Et puis quelqu'un vient à la rescousse, oui, vous l'avez deviné, encore Java, à savoir JNI (interface native java). JNI vous permet d'appeler des méthodes C natives à l'intérieur de la JVM. Le code est émis d'une manière spéciale sous la forme d'un objet partagé de la bibliothèque, que la JVM charge ensuite, tandis que les méthodes qui étaient dans la bibliothèque sont mappées aux méthodes java à l'intérieur de la classe déclarées avec le modificateur natif.

Eh bien, ok, nous écrivons une classe (en fait, ce n'est qu'une pièce):

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() 
    {
        
    }
}

Après cela, compilez-le et récupérez le fichier h généré de la future bibliothèque:

#  
javac Posix.java

#   Posix.h        JNI
javah Posix

Après avoir reçu le fichier d'en-tête, nous écrivons le corps de chaque méthode:

#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);
}

et compiler la bibliothèque

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

Pour que Java charge la bibliothèque native, elle doit être trouvée par le système ld selon toutes les règles Linux. De plus, Java possède un ensemble de propriétés qui contiennent les chemins d'accès aux recherches de bibliothèque. La façon la plus simple de travailler dans Oracle est de mettre notre bibliothèque dans $ ORACLE_HOME / lib.

Et après avoir créé la bibliothèque, nous devons compiler la classe à l'intérieur de la base de données et la publier en tant que package plsql. Il existe 2 options pour créer des classes Java dans la base de données:

  • charger le fichier de classe binaire via l'utilitaire loadjava
  • compiler le code de classe Ă  partir de la source Ă  l'aide de sqlplus

Nous utiliserons la deuxième méthode, bien qu'elles soient fondamentalement égales. Pour le premier cas, il était nécessaire d'écrire immédiatement tout le code de classe à l'étape 1, lorsque nous avons reçu une classe de stub pour le fichier h.

Pour créer une classe dans subd, une syntaxe spéciale est utilisée:

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

Lorsque la classe est créée, elle doit être publiée en tant que méthodes plsql, et là encore la syntaxe spéciale:

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

Lorsque vous essayez d'appeler des méthodes potentiellement dangereuses dans Java, une exécution est déclenchée qui indique qu'aucune autorisation Java n'a été émise pour l'utilisateur. Le chargement de méthodes natives est une autre opération dangereuse, car nous injectons du code superflu directement dans le processus de base de données (le même exploit qui a été annoncé dans l'en-tête).

Mais puisque la base de données est test, nous accordons une subvention sans aucun souci de connexion à partir de sys:

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

Le nom d'utilisateur du système est celui où j'ai compilé le code java et le paquet wrapper plsql.
Il est important de noter que lors du chargement d'une bibliothèque via un appel à System.loadLibrary, nous omettons le préfixe lib et l'extension so (comme décrit dans la documentation) et ne passons aucun chemin où chercher. Il existe une méthode System.load similaire qui ne peut charger une bibliothèque qu'en utilisant un chemin absolu.

Et puis 2 désagréables surprises nous attendent - j'ai atterri dans le prochain trou de lapin d'Oracle. Lors de l'émission d'une autorisation, une erreur se produit avec un message plutôt brumeux:

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

Le problème est googlé sur Internet et conduit à My Oracle Support (aka Metalink). Parce que Selon les règles d'Oracle, la publication d'articles à partir d'un Metalink n'est pas autorisée dans les sources ouvertes, je ne mentionnerai que le numéro de document - 259471.1 (ceux qui y ont accès pourront lire par eux-mêmes).

L'essence du problème est qu'Oracle ne nous laissera pas simplement autoriser le chargement de code tiers suspect dans notre processus. C'est logique.

Mais comme la base est testée et que nous avons confiance en notre code, nous autorisons le téléchargement sans craintes particulières.
Fuh, les mésaventures sont partout.

C'est vivant, vivant


Avec un souffle retenu, j'ai décidé d'essayer de donner vie à mon Frankenstein.
Nous commençons la base de données avec le libfaketime préchargé et le décalage 0.
Connectez-vous à la base de données et appelez le code qui affiche simplement l'heure avant et après avoir changé la variable d'environnement:

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;
/


Ça marche, bon sang! Honnêtement, je m'attendais à d'autres surprises, telles que des erreurs ORA-600. Cependant, l'alerte avait le numéro complet et le code a continué à fonctionner.
Il est important de noter que si la connexion à la base de données est effectuée comme dédiée, puis une fois la connexion terminée, le processus sera détruit et il n'y aura aucune trace. Mais si nous utilisons des connexions partagées, dans ce cas, un processus prêt à l'emploi est alloué à partir du pool de serveurs, nous modifions l'heure dans celui-ci via des variables d'environnement et lorsqu'il est déconnecté, il restera modifié à l'intérieur du processus. Et lorsqu'une autre session de base de données tombe dans le même processus serveur, elle recevra le mauvais moment à sa grande surprise. Par conséquent, à la fin de la session de test, il est préférable de toujours remettre l'heure à zéro.

Conclusion


J'espère que l'histoire était intéressante (et peut-être même utile à quelqu'un).

Les codes sources sont tous disponibles sur Github .

La documentation de libfaketime aussi .

Comment faites-vous les tests? Et comment créer des bases de données de développement et de test dans une entreprise?

Bonus pour ceux qui lisent jusqu'au bout


All Articles