Crash when iterating over notes by iterating over sections when a section is 8-byte aligned and padded
haadr opened this issue · comments
All below is tested with pyelftools Version: 0.30.
Context
As in #534, I've got a libqtwebenginequickplugin.so
where qt has injected a .note.qt.metadata
section and note.
# readelf --sections libqtwebenginequickplugin.so
There are 31 section headers, starting at offset 0x41f8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.pr[...] NOTE 00000000000002a8 000002a8
0000000000000030 0000000000000000 A 0 0 8
[ 2] .note.qt.metadata NOTE 00000000000002d8 000002d8
0000000000000078 0000000000000000 A 0 0 8
...
As we can see, the note is of size 120.
Bug ❓
When trying to iterate over the notes I get
Traceback (most recent call last):
File "pyelftools/elftools/construct/core.py", line 573, in _parse
subobj = self.subcon._parse(stream, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "pyelftools/elftools/construct/core.py", line 316, in _parse
return _read_stream(stream, self.length)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "pyelftools/elftools/construct/core.py", line 293, in _read_stream
raise FieldError("expected %d, found %d" % (length, len(data)))
elftools.construct.core.FieldError: expected 1, found 0
Using pyelftools to inspect the note before the crash, I see
Note section ".note.qt.metadata" at offset 728 with size 120
Name: qt-project!
Type: 1951465473
Namesz: 12
Descsz: 89
Desc (89): 01060601bf02782c6f72672e71742d70726f6a6563742e51742e51516d6c457874656e73696f6e496e746572666163652f312e3003715174576562456e67696e65506c7567696e63757269816b5174576562456e67696e65ff
It looks like the section (and desc?) is padded with a total of 7 bytes.
hexdump -C .note.qt.metadata
00000000 0c 00 00 00 59 00 00 00 01 00 51 74 71 74 2d 70 |....Y.....Qtqt-p|
00000010 72 6f 6a 65 63 74 21 00 01 06 06 01 bf 02 78 2c |roject!.......x,|
00000020 6f 72 67 2e 71 74 2d 70 72 6f 6a 65 63 74 2e 51 |org.qt-project.Q|
00000030 74 2e 51 51 6d 6c 45 78 74 65 6e 73 69 6f 6e 49 |t.QQmlExtensionI|
00000040 6e 74 65 72 66 61 63 65 2f 31 2e 30 03 71 51 74 |nterface/1.0.qQt|
00000050 57 65 62 45 6e 67 69 6e 65 50 6c 75 67 69 6e 63 |WebEnginePluginc|
00000060 75 72 69 81 6b 51 74 57 65 62 45 6e 67 69 6e 65 |uri.kQtWebEngine|
00000070 ff 00 00 00 00 00 00 00 |........|
00000078
Fix ❓
It seems to me that
- iter_notes takes an entire section
- iter_notes tries to parse notes until offset >= end
pyelftools/elftools/elf/notes.py
Line 17 in 01621ab
- for each note, the offset is incremented with an assumed 4-byte alignment (?)
pyelftools/elftools/elf/notes.py
Line 59 in 01621ab
Since this section is 8-byte aligned, the last loop will try to parse 4 bytes only (the rest of the padding), which is not enough for a complete note.
The following is a quick fix that worked for me, but I don't know whether it makes sense/is the best way to solve that. I reasoned that with an empty name and desc, 12 bytes is the smallest a note could be.
diff --git a/elftools/elf/notes.py b/elftools/elf/notes.py
index c708cbf..bf2895b 100644
--- a/elftools/elf/notes.py
+++ b/elftools/elf/notes.py
@@ -15,6 +15,9 @@ def iter_notes(elffile, offset, size):
"""
end = offset + size
while offset < end:
+ if end - offset < 12:
+ break
+
note = struct_parse(
elffile.structs.Elf_Nhdr,
elffile.stream,
Reproduction
Script used to reproduce error (almost same as elf_notes.py example)
from __future__ import print_function
import sys
# If pyelftools is not installed, the example can also run from the root or
# examples/ dir of the source distribution.
sys.path[0:0] = ['.', '..']
from elftools.elf.elffile import ELFFile
from elftools.elf.sections import NoteSection
from elftools.common.utils import bytes2hex
def process_file(filename):
print('Processing file:', filename)
with open(filename, 'rb') as f:
for sect in ELFFile(f).iter_sections():
if not isinstance(sect, NoteSection):
continue
print(' Note section "%s" at offset 0x%.8x with size %d' % (
sect.name, sect.header['sh_offset'], sect.header['sh_size']))
for note in sect.iter_notes():
print(' Name:', note['n_name'])
print(' Type:', note['n_type'])
desc = note['n_desc']
if note['n_type'] == 'NT_GNU_ABI_TAG':
print(' Desc: %s, ABI: %d.%d.%d' % (
desc['abi_os'],
desc['abi_major'],
desc['abi_minor'],
desc['abi_tiny']))
elif note['n_type'] in {'NT_GNU_BUILD_ID', 'NT_GNU_GOLD_VERSION', 'NT_GNU_PROPERTY_TYPE_0'}:
print(' Desc:', desc)
else:
print(' Desc:', "".join(map(chr, desc)))
if __name__ == '__main__':
if sys.argv[1] == '--test':
for filename in sys.argv[2:]:
process_file(filename)
output from readelf -a is attached.
shared object file is attached. it's libqtwebenginequickplugin.so from a standard openSUSE Tumbleweed install.
for each note, the offset is incremented with an assumed 4-byte alignment (?)
The padding for note data should definitely always be 4-bytes. See "Note Section" chapter of the ELF spec.
Since this section is 8-byte aligned, the last loop will try to parse 4 bytes only (the rest of the padding), which is not enough for a complete note.
Ah I see, the alignment on the segment is 8 bytes, causing 4 additional 0x00 bytes after the 4 note padding bytes.
What I am not sure about is whether the p_memsz
size is supposed to include or exclude padding. In the .elf you provided, the additional padding is included in the size.
That said, I feel like it's probably a good idea to stop trying to parse notes if there are not enough bytes left.
I'll work on a PR to fix this in the next days.