Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSEED: Segfault reading truncated file #1728

Merged
merged 6 commits into from Mar 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.txt
Expand Up @@ -93,8 +93,9 @@ master: (doi: 10.5281/zenodo.165135)
* Update to libmseed v2.19.2 (see #1703).
* Correctly read MiniSEED files with a data offset of 48 bytes (see #1540).
* InternalMSEEDReadingError now called InternalMSEEDError and
InternalMSEEDReadingWarning now called of InternalMSEEDWarning as both
can now also be raised in non-reading contexts (see #1658).
InternalMSEEDReadingWarning now called of InternalMSEEDWarning as both
can now also be raised in non-reading contexts (see #1658).
* Should no-longer segfault with arbitrarily truncated files (see #1728).
- obspy.io.nlloc:
* Set preferred origin of event (see #1570)
- obspy.io.nordic:
Expand Down
15 changes: 15 additions & 0 deletions obspy/io/mseed/core.py
Expand Up @@ -257,6 +257,21 @@ def _read_mseed(mseed_object, starttime=None, endtime=None, headonly=False,
else:
bo = None

# Determine total size. Either its a file-like object.
if hasattr(mseed_object, "tell") and hasattr(mseed_object, "seek"):
cur_pos = mseed_object.tell()
mseed_object.seek(0, 2)
length = mseed_object.tell() - cur_pos
mseed_object.seek(cur_pos, 0)
# Or a file name.
else:
length = os.path.getsize(mseed_object)

if length < 128:
msg = "The smallest possible mini-SEED record is made up of 128 " \
"bytes. The passed buffer or file contains only %i." % length
raise ValueError(msg)

info = util.get_record_information(mseed_object, endian=bo)

# Map the encoding to a readable string value.
Expand Down
12 changes: 7 additions & 5 deletions obspy/io/mseed/src/obspy-readbuffer.c
Expand Up @@ -375,12 +375,14 @@ readMSEEDBuffer (char *mseed, int buflen, Selections *selections, flag
msr_free(&msr);
break;
}
// msr_parse() returns > 0 if a data record has been detected but the buffer either has not enough
// data (this cannot happen with ObsPy's logic) or the last record has no Blockette 1000 and it cannot
// determine the record length because there is no next record (this can happen in ObsPy) - handle that
// case by just calling msr_parse() with an explicit record length set.
// Data missing at the end.
else if (retcode > 0 && retcode >= (buflen - offset)) {
log_error(MS_ENDOFFILE, offset);
msr_free(&msr);
break;
}
// Lacking Blockette 1000.
else if ( retcode > 0 && retcode < (buflen - offset)) {

// Check if the remaining bytes can exactly make up a record length.
int r_bytes = buflen - offset;
float exp = log10((float)r_bytes) / log10(2.0);
Expand Down
91 changes: 91 additions & 0 deletions obspy/io/mseed/tests/test_mseed_special_issues.py
Expand Up @@ -1052,6 +1052,97 @@ def test_reading_file_with_data_offset_of_48(self):
st[0].data[:6],
[337, 396, 454, 503, 547, 581])

def test_reading_truncated_miniseed_files(self):
"""
Regression test to guard against a segfault.
"""
filename = os.path.join(self.path, 'data',
'BW.BGLD.__.EHE.D.2008.001.first_10_records')

with io.open(filename, 'rb') as fh:
data = fh.read()

data = data[:-257]
# This is the offset for the record that later has to be recorded in
# the warning.
self.assertEqual(len(data) - 255, 4608)

# The file now lacks information at the end. This will read the file
# until that point and raise a warning that some things are missing.
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with io.BytesIO(data) as buf:
st = _read_mseed(buf)
self.assertEqual(len(st), 1)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, InternalMSEEDWarning)
self.assertEqual("readMSEEDBuffer(): Unexpected end of file when "
"parsing record starting at offset 4608. The rest of "
"the file will not be read.", w[0].message.args[0])

def test_reading_truncated_miniseed_files_case_2(self):
"""
Second test in the same vain as
test_reading_truncated_miniseed_files. Previously forgot a `<=` test.
"""
filename = os.path.join(self.path, 'data',
'BW.BGLD.__.EHE.D.2008.001.first_10_records')

with io.open(filename, 'rb') as fh:
data = fh.read()

data = data[:-256]
# This is the offset for the record that later has to be recorded in
# the warning.
self.assertEqual(len(data) - 256, 4608)

# The file now lacks information at the end. This will read the file
# until that point and raise a warning that some things are missing.
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with io.BytesIO(data) as buf:
st = _read_mseed(buf)
self.assertEqual(len(st), 1)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, InternalMSEEDWarning)
self.assertEqual("readMSEEDBuffer(): Unexpected end of file when "
"parsing record starting at offset 4608. The rest of "
"the file will not be read.", w[0].message.args[0])

def test_reading_less_than_128_bytes(self):
"""
128 bytes is the smallest possible MiniSEED record.

Reading anything smaller should result in an error.
"""
filename = os.path.join(self.path, 'data',
'BW.BGLD.__.EHE.D.2008.001.first_10_records')

with io.open(filename, 'rb') as fh:
data = fh.read()

# Reading at exactly 128 bytes offset will result in a truncation
# warning.
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with io.BytesIO(data[:128]) as buf:
st = _read_mseed(buf)
self.assertEqual(len(st), 0) # nothing is read here.
self.assertGreaterEqual(len(w), 1)
self.assertIs(w[-1].category, InternalMSEEDWarning)
self.assertEqual("readMSEEDBuffer(): Unexpected end of file when "
"parsing record starting at offset 0. The rest of "
"the file will not be read.", w[-1].message.args[0])

# Reading anything less result in an exception.
with self.assertRaises(ValueError) as e:
with io.BytesIO(data[:127]) as buf:
_read_mseed(buf)
self.assertEqual(
e.exception.args[0],
"The smallest possible mini-SEED record is made up of 128 bytes. "
"The passed buffer or file contains only 127.")


def suite():
return unittest.makeSuite(MSEEDSpecialIssueTestCase, 'test')
Expand Down