Linux Embedded

Le blog des technologies libres et embarquées

Gestion de versions des bibliothèques partagées

Tout code est susceptible au changement, avec pour objectif d’ajouter des fonctionnalités, de résoudre des BUGS ou même d’aller jusqu’a modifier les interfaces (altérer les prototypes des fonctions).

Généralement plus un code est utilisé par la communauté, plus il est déconseillé de modifier les interfaces lors d’une évolution. Cependant, la rétrocompatibilité reste floue pour certains développeurs surtout lors de la mise à jour des bibliothèques partagées.

Dans cet article, nous découvrirons comment maintenir les bibliothèques pour pouvoir les distribuer et les modifier sans avoir à recompiler les anciens binaires. Ces derniers pourront ainsi bénéficier d’une amélioration de performances ou de corrections de failles de sécurité.

Pourquoi la gestion de version des bibliothèques?

Pour répondre à cette question il est important de connaître quelques concepts liés aux bibliothèques partagées.

Quelques conventions à retenir

  • Une bibliothèque partagée doit être compilée comme ceci :
$ gcc -Wall -fPIC -c bibliotheque_dynamique.c
$ gcc -shared bibliotheque_dynamique.o -o bibliotheque_dynamique.so

L’argument « -shared » est un flag linker utilisé pour créer les bibliothèques partagées.

Remarque : le flag du compilateur « -fPIC » ne doit pas être associé à la création des bibliothèques partagées (il est possible de l’utiliser pour créer des bibliothèques statiques).

  • Par convention, le nom d’une bibliothèque partagée prend la forme : libnombibliotheque.so.<M>.<m>.<p> (par exemple : libssl.so.1.0.0).

N.B : les symboles M, m et p désignent les nombres Majeur, mineur et patch respectivement. Ces derniers précisent l’impact du changement de version sur le code, par exemple : la modification de la signature d’une fonction est un changement de type majeur alors que l’optimisation d’un calcul dans une routine a des conséquences mineures.

Alors pourquoi versionner?

Pour démontrer l’importance du versionnage, nous allons créer une simple bibliothèque partagée (et suivre son évolution).

Exemple pratique

Pour les besoins d’un outil de monitoring du système, nous implémentons une bibliothèque qui permet de lister les différents utilisateurs connectés sur le système.

Solution proposée

Sous Linux, il existe deux fichiers qui se chargent de cette tâche :

  • /var/run/utmp : qui liste les utilisateurs connectés (utilisé par l’utilitaire who).
  • /var/log/wtmp : sauvegarde chaque connexion et déconnexion établie par un utilisateur (utilisé par l’utilitaire « last »).

Première release : libusrmgr.so.1.0.0

usrmgr.h : fichier header de la bibliothèque « libusrmgr.so.1.0.0 » :

#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>

/* Retourne la liste des utilisateurs connectés */
void getLoggedUsers();

usrmgr.c : implémentation de la bibliothèque « libusrmgr.so.1.0.0 » :

#include "usrmgr.h"

/* Retourne la liste des utilisateurs connectés */
void getLoggedUsers(){
    struct utmpx *utmpUser;

    setutxent(); // Ouverture du fichier /var/run/utmp

    /* Parcours de la liste des utilisateurs */
    while ((utmpUser = getutxent()) != NULL) {
        printf("Utilisateur : %s <=> PID : %ld at : %s", utmpUser->ut_user,
         (long) utmpUser->ut_pid, ctime((time_t *) &(utmpUser->ut_tv.tv_sec)));
    }
}

premierBinaire.c : binaire client (qui va utiliser notre bibliothèque partagée).

#include "usrmgr.h"

int main(){
    printf("La liste des utilisateurs connectés : \n");
    getLoggedUsers();

    return EXIT_SUCCESS;
}

Il ne reste plus qu’à réaliser la compilation et le test :

  • Compiler une bibliothèque partagée :
$ gcc -Wall -fPIC -c usrmgr.c
$ gcc -shared usrmgr.o -o libusrmgr.so.1.0.0
  • Compiler le binaire et tester l’exécution :

Deuxième release : libusrmgr.so.2.0.0

Au fil du temps, on se rend compte des besoins suivants :

  • Le fichier utmp (ouvert avec setutxent()) n’est pas fermé après avoir obtenu la liste des utilisateurs (l’ancien binaire doit bénéficier de cette mise à jour).
  • Pour des raisons de sécurité, l’utilisateur « Pierre » ne doit pas être affiché (l’ancien binaire doit bénéficier de cette mise à jour).
  • La fonction getLoggedUsers() doit retourner le nombre d’utilisateurs. Pour cela l’ABI de la fonction doit changer (obligation de passer à la version 2.0.0 de la bibliothèque).
  • Introduire la possibilité de filtrer sur un seul utilisateur.

Avec ce simple exemple, on se retrouve contraint de maintenir plusieurs versions de la bibliothèque, ce qui n’est pas possible (les anciens binaires ne vont bénéficier d’aucune mise à jour).

La prochaine release (incompatible avec l’ancien binaire) sera comme ceci :

usrmgr.h : fichier header de la bibliothèque « libusrmgr.so.2.0.0 » :

#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <string.h>

#define HIDDEN_USER "Pierre"

/* Affiche et retourne le nombre des utilisateurs connectés */
int getLoggedUsers(char filterUser[]);

usrmgr.c : implémentation de la bibliothèque « libusrmgr.so.2.0.0 » :

#include "usrmgr.h"

int getLoggedUsers(char filterUser[]){
    int usr_nb = 0;
    struct utmpx *utmpUser;
    setutxent(); // Ouverture du fichier /var/run/utmp

    /* Parcours de la liste des utilisateurs */
    while ((utmpUser = getutxent()) != NULL) {
        if(strcmp(utmpUser->ut_user, HIDDEN_USER) == 0){
            // Reprendre au début de la boucle pour ne pas inclure l'utilisateur.
            continue;
        }
        usr_nb++;
        if(strcmp(utmpUser->ut_user,filterUser) == 0){ // Si filtre utilisateur
             printf("Filtre utilisateur : %s <=> PID : %ld at : %s est connecté!\n",
                utmpUser->ut_user, (long) utmpUser->ut_pid,
                ctime((time_t *) &(utmpUser->ut_tv.tv_sec)));
            break;
        } else if(strcmp(filterUser,"") == 0){
             printf("Utilisateur : %s <=> PID : %ld at : %s\n", utmpUser->ut_user,
                (long) utmpUser->ut_pid, ctime((time_t *) &(utmpUser->ut_tv.tv_sec)));
        }
    }

    endutxent(); // Fermer le fichier /var/run/utmp
    return usr_nb;
}

deuxiemeBinaire.c : le binaire doit être mis à jour comme ceci :

#include "usrmgr.h"

int main(){
    printf("La liste des utilisateurs connectés : \n");
    getLoggedUsers("");

    printf("\nFiltrer sur l'utilisateur 'jugurtha' : \n");
    getLoggedUsers("jugurtha");

    return EXIT_SUCCESS;
}

La compilation et l’exécution du nouveau binaire sont réalisées comme suit :

Il est temps d’essayer d’exécuter l’ancien binaire (qu’il faut copier dans le répertoire ou se trouve libusrmgr.so.2.0.0) avec la nouvelle bibliothèque :

Important

L’utilitaire ldd permet de lister les bibliothèques importées par une application. Voici le résultat de la commande sur nos deux programmes :

On peut voir clairement que le chemin vers la bibliothèque « libusrmgr.so.1.0.0 » n’est pas
résolu (pour le premier binaire).

C’est cela qu’on appelle le problème de version des bibliothèques partagées. Une notion méconnue de certains développeurs.

Gérer le versionnement de ses bibliothèques

Il existe deux méthodes très appréciées en pratique :

  • Méthode du lien symbolique de type « soname » : se limite aux changements mineurs et aux patchs (donc les nombres mineur et patch d’une version d’une bibliothèque).
  • Méthode « gestion de symboles » : plus puissante (et un peu plus complexe) que la précédente mais permet de gérer tout type de changement en gardant une rétrocompabilité irréprochable (résiste même au changement de l’ABI et des interfaces). C’est cette méthode qu’utilise la fameuse bibliothèque glibc.

La méthode du lien type « soname »

Cette méthode part du principe de réduction des informations de versionnage qui apparaissent dans le nom des bibothèques partagées.

Pour cela le binaire est compilé avec un lien symbolique intermédiaire (appelé le lien « soname ») qui ne contient que le nombre majeur comme information de versionnage (qui lui même fait référence à la vraie bibliothèque). La figure suivante illustre le processus :

Comme le montre clairement le schéma précédent, plus besoin de recompiler le binaire pour référencer la nouvelle version de la bibliothèque. On peut encore aller plus loin, un lien symbolique (optionnel mais recommandé par convention) peut être inséré entre le binaire et le lien symbolique de type soname. Cela permet de compiler le binaire d’une manière générique : $ gcc -ltestbiblio … à la place de $ gcc -l:libtestbiblio.so.1.

Le schéma final qui décrit la méthode soname est le suivant :

Ce qui fait maintenant que tous les binaires (tant que le nombre majeur n’est pas changé) peuvent fonctionner avec la nouvelle bibliothèque sans avoir à les recompiler.

Exemple 1 : release de la bibliothèque libfilemanager.so.1.0.0

Nous prendrons comme exemple une simple bibliothèque qui affiche les statistiques d’un fichier.

filemanager.h : header de la bibliothèque « libfilemanager.so.1.0.0 » :

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>

void getFileStats(const char* filePath);

filemanager.c : implémentation de la bibliothèque « filemanager.so.1.0.0 » :

#include "filemanager.h"

void getFileStats(const char* filePath){
    struct stat fileStat;
    int fileError = 0;
    
    fileError = stat(filePath, &fileStat);

    if(fileError < 0){
        perror("erreur fonction stat");
        return;
    }
    // Check if the provided file is a regular file
    if((fileStat.st_mode & S_IFMT) == S_IFREG)
        printf("Nom du Fichier %s => Taille : %ld bytes\n", filePath, fileStat.st_size);
    else
        printf("Le fichier n'est pas de type S_IFREG\n");
}

La compilation et la création du lien symbolique doivent se faire comme ceci :

premierClient.c : implémentation du binaire client « premierClient » :

#include "filemanager.h"

int main(int argc, char *argv[]){
    
    if(argc <2){
        printf("Usage : %s <Nom_Fichier>\n", argv[0]);
        return 1;    
    }
    // Get file statistics        
    getFileStats(argv[1]);

    return 0;
}

Exemple 2 : release de la bibliothèque libfilemanager.so.1.1.0

Après quelque temps, on se rend compte qu’il est nécessaire d’avoir une fonction pour lister les fichiers contenus dans un dossier, on doit introduire l’interface « getFilesInFolder » et faire une nouvelle release « libfilemanager.so.1.1.0 ».

filemanager.h : header de la bibliothèque « libfilemanager.so.1.1.0 » :

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>

void getFileStats(const char* filePath);

void getFilesInFolder(const char* folderPath);

filemanager.c : implémentation de la bibliothèque « filemanager.so.1.1.0 » :

#include "filemanager.h"

void getFileStats(const char* filePath){
    struct stat fileStat;
    int fileError = 0;
    
    fileError = stat(filePath, &fileStat);

    if(fileError < 0){
        perror("erreur fonction stat");
        return;
    }
    // Check if the provided file is a regular file
    if((fileStat.st_mode & S_IFMT) == S_IFREG)
        printf("Nom du Fichier %s => Taille : %ld bytes\n", filePath, fileStat.st_size);
    else
        printf("Le fichier n'est pas de type S_IFREG\n");
}

void getFilesInFolder(const char* folderPath){
    DIR* folder = NULL;
    struct dirent* fileToRead = NULL;
    int fileCounter = 0;

    folder = opendir(folderPath); // open folder

    if (folder == NULL){
        perror("Fonction opendir");
        return;
    }
    
    while ((fileToRead = readdir(folder)) != NULL){
        fileCounter++;
        // display discovered files
        printf("%d - %s\n", fileCounter, fileToRead->d_name);
    }

    closedir(folder);
}

deuxiemeClient.c : implémentation du binaire client « deuxiemeClient » :

#include "filemanager.h"

int main(int argc, char *argv[]){
    
    if(argc <2){
        printf("Usage : %s <Nom_Dossier>\n", argv[0]);
        return 1;
    }
            
    getFilesInFolder(argv[1]);

    return 0;
}
  • Supprimer l’ancien lien symbolique :
$ rm -rf libfilemanager.so
  • Recompiler la bibliothèque et créer un nouveau lien de type « soname » comme décrit précédemment (exemple 1).
  • Compiler le binaire client :

N.B : L’ancien binaire ne sera pas affecté par les changements, le loader va toujours charger libfilemanager.so.1 (qui pointe désormais sur libfilemanager.1.1.0) comme le montre la figure ci-dessous :

La méthode « soname » nous a permis en toute facilité d’apporter des modifications à notre bilbiothèque, le tout en restant compatible avec l’ancien binaire.

La méthode « gestion de symboles »

Cette méthode est plus puissante et plus complète que la précédente (elle est d’ailleurs utilisée par glibc depuis la version 2.6). En effet, les changements ne sont pas restreints au nombre mineur (et nombre patch) d’une bibliothèque.
La méthode « gestion de symboles » s’appuie aussi sur la méthode du lien du type « soname » en garantissant une rétrocompatibilité antérieure pour tout type de modifications.
Le schéma vu précédemment peut être redéfini comme suit :

N.B : il n’est plus nécéssaire d’avoir un lien symbolique (entre le binaire et le lien de type « soname ») car le nom du lien type « soname » ne contient plus le nombre majeur.

La méthode « gestion de symboles » introduit deux briques supplémentaires pour combler les limitations de la méthode du lien type « soname » :

  • Le gestionnaire de version « linker script » : fichier parsé par le linker; ce dernier décrit les fonctions, leurs versions (en indiquant la version à utiliser pour les anciennes et les prochaines versions de la bibliothèque) et leur visibilité (les fonctions à exporter au binaire du client).
    N.B : le gestionnaire de version est suffisant pour gérer les changements sur les nombres mineur et patch.
  • la directive « .symver » (directive supportée par GCC) : utilisée lors d’une altération du prototype d’une fonction (incluant une mise à jour du nombre majeur de la bibliothèque).
    N.B : cette directive n’est introduite que lors d’un changement de la définition des interfaces (c’est à dire la mise à jour du nombre majeur).

Comme dans le cas des parties précédentes, nous partirons sur un exemple qui va évoluer au fur et à mesure.

Exemple 1 : première release de la bibliothèque libnbgenerator.so.1.0.0

Pour les besoins d’une gestion de mot de passe, nous implémentons une bibliothèque qui peut générer des nombres pseudo-aléatoires.

Une simple implémentation peut être la suivante :

nbgenerator.h : header de la bibliothèque « libnbgenerator.so.1.0.0 » :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define DEFAULT_MAX_NUMBER_LIMIT 1000

int getPseudoNumber(int numLimit);

nbgenerator.c : implémentation de la bibliothèque « libnbgenerator.so.1.0.0 » :

#include "nbgenerator.h"

int getPseudoNumber(int numLimit){
    int rNumber = 0;
    srand(time(NULL));
    if(numLimit > 0)
        rNumber = rand() % numLimit;
    else
        rNumber = rand() % DEFAULT_MAX_NUMBER_LIMIT;
    return rNumber;
}

script_version : et enfin le « linker script » pour assister le linker à la création d’une bibliothèque conforme à la méthode « gestion de symboles ». Dans ce cas, il indique que seul getPseudoNumber sera exporté et vu par le binaire du client.

NBGENERATOR_1.0{
    global:
        getPseudoNumber;
    local:
        *;
};

N.B : généralement il est n’est pas nécessaire d’inclure le nombre patch dans le « linker script » car ce genre de changement n’impacte pas la rétrocompatibilité (NBGENERATOR_1.0 n’est qu’un symbole dans l’entête de la bibliothèque comme on verra par la suite) ; cependant, on peut très bien écrire NBGENERATOR_1.0.0.

la compilation de la bibliothèque se fera de la manière suivante :

PC@PC ~ $ gcc -Wall -fPIC -c nbgenerator.c
PC@PC ~ $ gcc -shared nbgenerator.o -Wl,-soname,libnbgenerator.so -Wl,--version-script,script_version -o libnbgenerator.so.1.0.0
PC@PC ~ $ ldconfig -n .

Remarque : Il est obligatoire de passer « -Wl, option » comme option de compilation pour modifier le comportement du linker (pour tenir compte du fichier de version « linker script »).

Grâce au « linker script », le linker insère 3 sections dans l’entête de la bibliothèque comme le montre la figure suivante :

$ readelf -V nbgenerator.so

Version symbols section '.gnu.version' contains 12 entries:
 Addr: 00000000000003f2  Offset: 0x0003f2  Link: 3 (.dynsym)
  000:   0 (*local*)       0 (*local*)       0 (*local*)       3 (GLIBC_2.2.5)
  004:   0 (*local*)       3 (GLIBC_2.2.5)   0 (*local*)       0 (*local*)    
  008:   3 (GLIBC_2.2.5)   3 (GLIBC_2.2.5)   2 (NBGENERATOR_1.0)     2 (NBGENERATOR_1.0)  

Version definition section '.gnu.version_d' contains 2 entries:
  Addr: 0x0000000000000410  Offset: 0x000410  Link: 4 (.dynstr)  000000: Rev: 1  Flags: BASE   Index: 1  Cnt: 1  Name: nbgenerator.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: NBGENERATOR_1.0
  Version definition past end of section

Version needs section '.gnu.version_r' contains 1 entries:
 Addr: 0x0000000000000448  Offset: 0x000448  Link: 4 (.dynstr)
  000000: Version: 1  File: libc.so.6  Cnt: 1
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 3
  • .gnu.version : contient les informations de version globale.
  • .gnu.version_d : affiche les informations de version relative à cette bibliothèque.
  • .gnu.version_r : permet de suivre les informations de version relatives aux bibliothèques référencées par cette bibliothèque.

N.B : ces champs liés à la version seront recopiés dans le binaire du client lors de la compilation, ce qui permettra au loader de charger les bonnes versions de fonctions adaptées pour chaque binaire.

La bibliothèque est prête, il ne reste plus qu’à créer un client pour l’exploiter.

premierClient.c : premier binaire client.

#include <stdio.h>
#include "nbgenerator.h"

int main(){
    printf("Le nombre aléatoire est %d\n", getPseudoNumber(1500));
    return 0;
}

Exemple 2 : deuxième release de libnbgenerator.so.1.1.0

En plus des nombres pseudo-aléatoires, il est requis d’avoir une interface pour la génération de chaines de caractères contenant des mots de passe.
Cela peut se faire comme indiqué :

nbgenerator.h : header de la bibliothèque « libnbgenerator.so.1.1.0 » :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define DEFAULT_MAX_NUMBER_LIMIT 1000
#define MAX_PASSWORD_LENGTH 20

int getPseudoNumber(int numLimit);

/* generationMotDePasse has a local symbol */
char *generationMotDePasse(int nbCharacters);

/* generationMotDePasse has a global symbol (exported to client binary) */
char* getRandomPassword(int nbCharacters);

nbgenerator.c : implémentation de la bibliothèque « libnbgenerator.so.1.1.0 » :

#include "nbgenerator.h"

char password[MAX_PASSWORD_LENGTH] = {0};
// getPseudoNumber : exported to client binary
int getPseudoNumber(int numLimit){
    int rNumber = 0;
    srand(time(NULL));
    if(numLimit > 0)
        rNumber = rand() % numLimit;
    else
        rNumber = rand() % DEFAULT_MAX_NUMBER_LIMIT;
    return rNumber;
}
// generationMotDePasse : not exported to client binary
char *generationMotDePasse(int nbCharacters){
    short i = 0;
    memset(password, 0, MAX_PASSWORD_LENGTH);
    
    srand(time(NULL));
    
    if((nbCharacters > MAX_PASSWORD_LENGTH) || (nbCharacters <= 0))
        nbCharacters = MAX_PASSWORD_LENGTH;
    /* Génération du mot de passe */
    for(i = 0; i< nbCharacters; i++){
        password[i] = 'A' + (rand() % nbCharacters);
    }
    return password;
}
// getRandomPassword : exported to client binary
char* getRandomPassword(int nbCharacters){
    return generationMotDePasse(nbCharacters);
}

script_version : Seules 2 des 3 fonctions seront exportées vers le binaire du client (getPseudoNumber() et getRandomPassword()) .

NBGENERATOR_1.0{
    global:
        getPseudoNumber;
    local:
        *;
};

NBGENERATOR_1.1{
    global:
        getRandomPassword;
    local:
        *;
};

deuxiemeClient.c : deuxième version du binaire du client

#include <stdio.h>
#include "nbgenerator.h"

int main(){
    printf("Le nombre aléatoire est %d\n", getPseudoNumber(1500));
    printf("Le mot de passe : %s\n", getRandomPassword(10));
    return 0;
}

la compilation se fait comme indiqué ci dessous :

Il est temps de tester l’ancien programme sans le recompiler :

Exemple 3 : troisième release de libnbgenerator.so.2.0.0

Il arrive parfois que la définition des interfaces soit modifiée (on dit souvent une altération de l’ABI). Sans la méthode « gestion de symboles« , ce genre de changement garantit de casser la rétro-compatibilité (c’est le cas d’une fonction qui ne retourne plus le même type ou pire encore, une interface qui doit être retirée de l’usage dans les nouveaux binaires).

Dans le cas de notre bibliothèque, on se retrouve vite limité par la fonction getRandomPassword() qui doit retourner en plus du mot de passe en clair, le mot de passe hashé et la fonction de hash.

La question qui se pose est comment faire compiler le code suivant :

// Ancien binaire
char* getRandomPassword(int nbCharacters);

// Nouveau binaire
struct PasswordHash getRandomPassword(int nbCharacters);

La réponse est la suivante :

nbgenerator.h : header de la bibliothèque « libnbgenerator.so.2.0.0 » :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#define _XOPEN_SOURCE
#include <unistd.h>
#include <crypt.h>

#define DEFAULT_MAX_NUMBER_LIMIT 1000
#define MAX_PASSWORD_LENGTH 20
#define ENCRYPTION_CIPHER_METHOD "crypt"

struct PasswordHash{
    char hashNameFunc[20];
    char* passwordToHash;
    char* hashedPassword;
} passHash;

int getPseudoNumber(int numLimit);
char *generationMotDePasse(int nbCharacters);

/* LIB_NBGENERATOR_VERSION_2_0 : Constant to be passed at compilation time */
/* Because only one header is allowed in case of same function signature */
#ifdef LIB_NBGENERATOR_VERSION_2_0
struct PasswordHash getRandomPassword(int nbCharacters);
#else
char* getRandomPassword(int nbCharacters);
#endif

nbgenerator.c : implémentation de la bibliothèque « libnbgenerator.so.2.0.0 » :

#include "nbgenerator.h"

char password[MAX_PASSWORD_LENGTH] = {0};

int getPseudoNumber(int numLimit){
    int rNumber = 0;
    srand(time(NULL));
    if(numLimit > 0)
        rNumber = rand() % numLimit;
    else
        rNumber = rand() % DEFAULT_MAX_NUMBER_LIMIT;
    return rNumber;
}

// generationMotDePasse : not exported to client binary
char *generationMotDePasse(int nbCharacters){
    short i = 0;
    memset(password, 0, MAX_PASSWORD_LENGTH);
    
    srand(time(NULL));
    
    if((nbCharacters > MAX_PASSWORD_LENGTH) || (nbCharacters <= 0))
        nbCharacters = MAX_PASSWORD_LENGTH;
    /* Génération du mot de passe */
    for(i = 0; i< nbCharacters; i++){
        password[i] = 'A' + (rand() % nbCharacters);
    }
    return password;
}

/* @ means use this for legacy (old binaries) */
__asm__(".symver getRandomPassword_1_1,getRandomPassword@NBGENERATOR_1.1");
char* getRandomPassword_1_1(int nbCharacters){
    return generationMotDePasse(nbCharacters);
}

/* @@ means default use this for new binaries */
__asm__(".symver getRandomPassword_2_0,getRandomPassword@@NBGENERATOR_2.0");
struct PasswordHash getRandomPassword_2_0(int nbCharacters){
    char passwordToHash[MAX_PASSWORD_LENGTH];
    char passwordSalt[6];
    
    strcpy(passwordToHash, generationMotDePasse(10));
    
    // $1$ allows to select md5 hashing algorithm    
    sprintf(passwordSalt, "$6$%s", generationMotDePasse(6));

    sprintf(passHash.hashNameFunc, "%s", ENCRYPTION_CIPHER_METHOD);
    
    /* Crypt returns a hash based on DES encryption */
    passHash.hashedPassword = crypt(passwordToHash, passwordSalt);

    return passHash;
}

Le code nécessite quelques petites remarques :

  • Les deux fonctions getRandomPassword doivent être déclarées avec des noms différents (par exemple getRandomPassword_1_1() pour la fonction getRandomPassword() associée au symbole NBGENERATOR_1.1). La directive symver est employée pour faire le mapping.
  • Le symbole @ se traduit par : appliquer cette régle à tout ancien binaire contrairement au @@ qui force le linker à utiliser cette fonction avec tous les nouveaux binaires qui seront compilés ultérieurement.

script_version : Il nous faut juste insérer le tag NBGENERATOR_2.0 dans le linker script :

NBGENERATOR_1.0{
    global:
        getPseudoNumber;
    local:
        *;
};

NBGENERATOR_1.1{
    global:
        getRandomPassword;
    local:
        *;
};

NBGENERATOR_2.0{
    global:
        getRandomPassword;
    local:
        *;
};

Et enfin, la dernière pièce de puzzle qui permet de tester notre bibliothèque, le troisième client :

troisiemeClient.c : troisième binaire client.

#include <stdio.h>
#include "nbgenerator.h"

int main(){
    struct PasswordHash pass = getRandomPassword(10);
    printf("Le nombre aléatoire est %d\n", getPseudoNumber(1500));
    printf("Le mot de passe avec hash : %s avec la fonction => %s\n",
            pass.hashedPassword, pass.hashNameFunc);

    return 0;
}

Il est temps d’exécuter et compiler notre programme :

Pour finir, voici l’exécution des trois programmes qui dépendent désormais de libnbgenerator.2.0.0 :

Important
Il est à noter que la méthode « gestion de symbole » peut augmenter la taille de la bibliothèque générée (comme dans notre exemple avec la présence des deux fonctions getRandomPassword()), c’est l’une des raisons qui rend la glibc volumineuse. Cet inconvénient est souvent négligé par rapport à ses nombreux avantages.

Conclusion

Dans cet article, nous avons découvert un concept avancé de la gestion des bibliothèques partagées. Il est primordial de concevoir ses bibliothèques avec l’une de ces deux méthodes : le lien type « soname » ou la gestion des symboles (cette dernière est à préconiser).
Ces méthodes permettent de maintenir et faire évoluer les bibliothèques, d’améliorer le code et d’apporter des corrections aux failles de sécurité sans avoir à recompiler le binaire du client final (c’est la retro-compatibilité).

Navigation de l'article