Détecter les dépassements de limites sous CentOS 6

Sommaire

Sous Linux, des limites de consommations de ressources permettent de s’assurer qu’un seul utilisateur ne sature pas un serveur partagé. Ces limites peuvent causer des pannes silencieuses.

Pour détecter ces dépassements, il est possible d’utiliser le démon auditd1. Celui-ci a la capacité de détecter des échecs d’appels systèmes.

Pour identifier l’appel système en cause et son code de retour, nous allons utiliser strace, qui permet de tracer tous les appels système d’une application.

Trop de fichiers ouverts

Ouverture de fichiers

Commençons par utiliser gawk par créer 1024 fichiers sans les fermer (ce qui est une très mauvaise idée de manière générale)…

BEGIN {
  # Création de 1024 fichiers
  for ( I=1 ; I <= 1024 ; I++ ){
    FICHIER = "test/" I
    print "TEST" > FICHIER
  }
}

Enregistrons le programme gawk dans le fichier nofile.awk, puis lançons le script awk sous strace :

mkdir -p test
strace gawk -f nofile.awk 2> nofile.log

Le fichier nofile.log est très long. En remontant à partir de la fin, on trouve :

open("tmp/1021", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 1023
fstat(1023, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
fcntl(1023, F_SETFD, FD_CLOEXEC)        = 0
fcntl(1023, F_GETFL)                    = 0x8001 (flags O_WRONLY|O_LARGEFILE)
fcntl(1023, F_GETFL)                    = 0x8001 (flags O_WRONLY|O_LARGEFILE)
fstat(1023, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc140450000
lseek(1023, 0, SEEK_CUR)                = 0
ioctl(1023, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffea301dcd0) = -1 ENOTTY (Inappropriate ioctl for device)
open("tmp/1022", O_WRONLY|O_CREAT|O_TRUNC, 0666) = -1 EMFILE (Too many open files)

Donc, l’appel système open reçoit l’erreur EMFILE (« trop de fichiers ouverts »). Ce qui nous donne la règle d’audit suivante :

-a always,exit -F arch=b64 -S open -F exit=-EMFILE -k nofile

Ouverture de connexions réseau

Le nombre maximum de fichiers ouverts peut concerner des connexions réseau. Essayons de reproduire cette erreur avec un petit programme gawk ouvrant 1024 connexions UDP :

BEGIN {
  # Ouverture de 1024 connexions UDP
  for ( I = 1 ; I <= 1024 ; I++ ){
    ADRESSE = "/inet/udp/0/127.0.0.1/" (1024 + I) 
    print "TEST" |& ADRESSE
  }
}

Après exécution de ce programme sous strace, on retombe sur la même erreur lors d’un appel de dup pour dupliquer un descripteur de fichiers.

socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) = 1023
bind(1023, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
connect(1023, {sa_family=AF_INET, sin_port=htons(1535), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
fstat(1023, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
fcntl(1023, F_SETFD, FD_CLOEXEC)        = 0
fcntl(1023, F_GETFL)                    = 0x2 (flags O_RDWR)
fstat(1023, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0fe056f000
lseek(1023, 0, SEEK_CUR)                = -1 ESPIPE (Illegal seek)
dup(1023)                               = -1 EMFILE (Too many open files)
close(1023)                             = 0

Ce qui nous donne une nouvelle version de la règle d’audit, qui gère à la fois l’ouverture d’un fichier (open) et la duplication d’un descripteur de fichiers (dup) :

-a always,exit -F arch=b64 -S open -S dup -F exit=-EMFILE -k nofile

Cela dit, cette erreur pourrait aussi être émise par l’appel système socket (l’appel système utilisé pour créer un connecteur réseau). Essayons d’ouvrir un premier fichier pour décaler les descripteurs de fichiers d’un cran (il y a à chaque fois un appel de socket pour ouvrir un connecteur réseau, suivi d’un appel de dup pour le dupliquer) :

BEGIN {
  # Ouverture d'un fichier
  print "TEST" > "test1"
  # Ouverture de 1024 connexions UDP
  for ( I = 1 ; I <= 1024 ; I++ ){
    ADRESSE = "/inet/udp/0/127.0.0.1/" (1024 + I)
    print "TEST" |& ADRESSE
  }
}

Après exécution du script gawk sous strace, on constate que cette fois, l’erreur tombe bien sur l’appel de socket. La sortie de strace montre que gawk tente de créer un connecteur réseau de plusieurs façons différentes, essayant en vain de contourner le problème :

socket(PF_NETLINK, SOCK_RAW, 0)         = -1 EMFILE (Too many open files)
socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP) = -1 EMFILE (Too many open files)
socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = -1 EMFILE (Too many open files)
socket(PF_NETLINK, SOCK_RAW, 0)         = -1 EMFILE (Too many open files)
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) = -1 EMFILE (Too many open files)
socket(PF_NETLINK, SOCK_RAW, 0)         = -1 EMFILE (Too many open files)
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) = -1 EMFILE (Too many open files)
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fffeb2c3570)       = 0

Ce qui nous donne la règle de détection suivante :

-a always,exit -F arch=b64 -S socket -F exit=-EMFILE -k nofile

Les règles de détection (trop de fichiers ouverts)

Après quelques recherches dans les pages de manuels, nous aboutissons aux règles d’audit suivantes, qui prennent en compte les différents appels systèmes pouvant rencontrer cette erreur :

-a always,exit -F arch=b64 -S open -S openat -S dup -F exit=-EMFILE -k nofile
-a always,exit -F arch=b32 -S open -S openat -S dup -F exit=-EMFILE -k nofile
-a always,exit -F arch=b64 -S socket                -F exit=-EMFILE -k nofile
-a always,exit -F arch=b32 -S socketcall -F a0=1    -F exit=-EMFILE -k nofile

Nous avons ajouté l’appel système openat (ouverture d’un fichier dans un répertoire spécifique) et les appels équivalents en 32 bits.

Trop de processus

Deuxième erreur très classique, trop de processus lancés en parallèle. Là aussi, nous allons reproduire l’erreur pour identifier les paramètres des règles d’audit :

BEGIN {
  # Lancer 1024 sous-processus « yes »
  for ( I = 1 ; I <= 1024 ; I++ ){
    # La chaîne contenant la commande doit être différente
    # à chaque fois, sinon gawk ne va créer qu'un seul
    # sous-processus
    COMMANDE = "yes #" I
    COMMANDE | getline
  }
}

Enregistrons le programme gawk dans le fichier nproc.awk, puis lançons le script awk sous strace :

ulimit -u 512
strace gawk -f nproc.awk 2> nproc.log

Note : nous réduisons le nombre maximum de processus pour éviter de saturer le nombre maximum de fichiers ouverts avant de saturer le nombre maximum de processus…

En compulsant la sortie de strace, on tombe sur un appel de clone, qui est utilisé pour créer un nouveau processus, échouant avec l’erreur EAGAIN (« veuillez ré-essayer plus tard ») :

clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f13af5b49d0) = 3549
close(978)                              = 0
fcntl(977, F_SETFD, FD_CLOEXEC)         = 0
ioctl(977, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffd6bb37d50) = -1 EINVAL (Invalid argument)
fstat(977, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(977, "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"..., 4096) = 4096
pipe([978, 979])                        = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f13af5b49d0) = -1 EAGAIN (Resource temporarily unavailable)

Comme précédemment, on peut en déduire (après quelques recherches complémentaires) les règles d’audit à appliquer :

-a always,exit -F arch=b64 -S clone -S fork -S vfork -F exit=-EAGAIN -k nproc
-a always,exit -F arch=b32 -S clone -S fork -S vfork -F exit=-EAGAIN -k nproc

Les règles de détection (finales)

En conclusion, voici les règles d’audit à appliquer pour détecter à la fois le dépassement du nombre maximum de fichiers ouverts et le dépassement du nombre maximum de processus :

-a always,exit -F arch=b64 -S open -S openat -S dup  -F exit=-EMFILE -k nofile
-a always,exit -F arch=b32 -S open -S openat -S dup  -F exit=-EMFILE -k nofile
-a always,exit -F arch=b64 -S socket                 -F exit=-EMFILE -k nofile
-a always,exit -F arch=b32 -S socketcall -F a0=1     -F exit=-EMFILE -k nofile
-a always,exit -F arch=b64 -S clone -S fork -S vfork -F exit=-EAGAIN -k nproc
-a always,exit -F arch=b32 -S clone -S fork -S vfork -F exit=-EAGAIN -k nproc

  1. Pour plus d’informations sur auditd, consultez l’article « Journalisez les actions de vos utilisateurs avec Auditd » de Chistian Perez. ↩︎