1. Contexte du problème
Récemment, un vidage de noyau s'est produit sur l'un de nos serveurs de recherche en ligne. Le noyau se trouve dans memset()
en raison de la tentative d'écriture à une adresse invalide, et a donc reçu le signal SIGSEGV. Les informations suivantes proviennent de dmsg :
is_searcher_ser[17405]: segfault at 000000002c32a668 rip 0000003da0a7b006 rsp 0000000053abc790 error 6
L'environnement de nos serveurs en ligne se présente comme suit :
- OS : RHEL 5.3
- Noyau : 2.6.18-131.el5.custom, x86_64 (64 bits)
- GCC : 4.1.2 20080704 (Red Hat 4.1.2-44)
- Glibc : glibc-2.5-49.6
Voici l'extrait de code correspondant :
CHashMap<…>::CHashMap(…)
{
…
typedef HashEntry *HashEntryPtr;
m_ppEntry = new HashEntryPtr[m_nHashSize]; // m_nHashSize is 389 when core
assert(m_ppEntry != NULL);
memset(m_ppEntry, 0x0, m_nHashSize*sizeof(HashEntryPtr)); // Core in this memset() invocation
…
}
Le code d'assemblage du code ci-dessus est :
…
0x000000000091fe9e <+110>: callq 0x502638 <_Znam@plt> // new HashEntryPtr[m_nHashSize]
0x000000000091fea3 <+115>: mov 0xc(%rbx),%edx // Get the value of m_nHashSize
0x000000000091fea6 <+118>: mov %rax,%rdi // Put m_ppEntry pointer to %rdi for later memset invocation
0x000000000091fea9 <+121>: mov %rax,0x20(%rbx) // Store the pointer to m_ppEntry member variable(%rbx holds the this pointer)
0x000000000091fead <+125>: xor %esi,%esi // Generate 0
0x000000000091feaf <+127>: shl $0x3,%rdx // m_nHashSize*sizeof(HashEntryPtr)
0x000000000091feb3 <+131>: callq 0x502b38 <memset@plt> // Call the memset() function
…
Dans la décharge centrale, l'assemblage de memset@plt
est :
(gdb) disassemble 0x502b38
Dump of assembler code for function memset@plt:
0x0000000000502b38 <+0>: jmpq *0x771b92(%rip) # 0xc746d0 <memset@got.plt>
0x0000000000502b3e <+6>: pushq $0x53
0x0000000000502b43 <+11>: jmpq 0x5025f8
End of assembler dump.
(gdb) x/ag 0x0000000000502b3e+0x771b92
0xc746d0 <memset@got.plt>: 0x3da0a7acb0 <memset>
(gdb) disassemble 0x3da0a7acb0
Dump of assembler code for function memset:
0x0000003da0a7acb0 <+0>: cmp $0x1,%rdx
0x0000003da0a7acb4 <+4>: mov %rdi,%rax
…
Pour l'analyse GDB ci-dessus, nous savons que l'adresse de memset()
a été résolu dans la table PLT de relocalisation. C'est-à-dire que le premier jmpq *0x771b92(%rip)
sautera directement à la première instruction de la fonction memset()
. De plus, le programme s'était déroulé sur près d'une journée en ligne, l'adresse de relocalisation de memset()
aurait dû être résolu plus tôt.
2. Phénomène étrange
Ce noyau a tiré sur l'instruction => 0x0000003da0a7b006 <+854>: mov %rdx,-0x8(%rdi)
dans le memset()
. En fait, il s'agit de l'instruction dans le memset()
pour définir le 0
à la position de début droite du tampon qui est le premier paramètre de la fonction memset()
.
En cas de carottage, dans le cadre 0, la valeur de $rdi
es 0x2c32a670
et $rax
es 0x2c32a668
. De l'analyse de l'assemblage et du test hors ligne, $rax
doit contenir le tampon source du memset
c'est à dire le premier paramètre de l'équation memset()
.
Donc, dans notre exemple, $rax
doit être la même que l'adresse de m_ppEntry
dont la valeur est stockée dans le this
objet ( this
Le pointeur est stocké dans %rbx
) d'abord avant d'être mis à zéro par memset
plus tard. Cependant, la valeur de m_ppEntry
es 0x2ab02c32a668
.
Ensuite, utilisez info files
Commande GDB à vérifier, l'adresse 0x2c32a668
est en effet invalide (non mappé), et l'adresse 0x2ab02c32a668
est une adresse valide.
3. Pourquoi c'est bizarre ?
L'endroit bizarre de ce noyau est que : Si l'adresse réelle de memset
a déjà été résolu (très très probablement), alors il n'y a que très peu d'instructions entre l'opération consistant à placer la valeur du pointeur dans le fichier m_ppEntry
et la tentative de memset
il. Et en fait, la valeur du registre $rax
(contenant l'adresse de la mémoire tampon transmise) ne sont pas modifiées du tout pendant ces instructions. Ainsi, comment m_ppEntry
n'est pas égal à $rax
?
Ce qui est bizarre Plus de est la suivante : lorsque le noyau, la valeur de $rax
( 0x2c32a668
) est en fait la valeur des 4 octets inférieurs de l'adresse de l'utilisateur. m_ppEntry
( 0x2ab02c32a668
). S'il y a effectivement une relation entre les deux valeurs, est-ce que la m_ppEntry
passé à memset
étant tronquée ? Cependant, les différentes instructions concernées utilisent toutes %rax
plutôt que %eax
. D'ailleurs, je ne peux pas reproduire ce problème hors ligne.
Donc,
1) Quelle adresse est valide ? Si 0x2c32a668
est valable ? Le tas est-il corrompu juste entre les différentes instructions ? Et comment paraphraser que la valeur de m_ppEntry
es 0x2ab02c32a668
et pourquoi les 4 octets inférieurs de ces deux valeurs sont identiques ?
2) Si 0x2ab02c32a668
est valide, pourquoi l'adresse est tronquée lorsqu'elle est transmise dans le système 64-bit memset()
? Dans quelles conditions cette erreur se produit-elle ? Je ne peux pas reproduire ce problème hors ligne. Ce problème est-il un bogue connu ? Je ne l'ai pas trouvé via Google.
3) Ou bien, est-ce dû à un problème de matériel ou d'alimentation pour faire en sorte que les 4 octets supérieurs de %rdi
transmis à memset
mis à zéro ? (Je suis très très réticent à le croire).
Enfin, tout commentaire sur ce noyau est apprécié.
Merci,
Gary Hu