Skip to content

Commit b0de7d4

Browse files
authored
fix(fits): Fix recursion stack overflow with too many header blocks (AcademySoftwareFoundation#5248)
FITS files are allowed to have multiple "header blocks". Unfortunately, as originally written, our fits input used *recursion* to read them. If there is a file with too many header blocks, it can crash with a stack overflow. Change to use a loop instead of recursion. And put a maximum iteration limit of 10000 headers in one file before giving up. (Note to reviewers: the diff looks bigger than it really is. By surrounding the contents of read_fits_header with a `for` loop, it reindented it, and that is the vast majority of the change. I wish GitHub review diffs more clearly visualized which changed lines are only whitespace changes.) Signed-off-by: Larry Gritz <lg@larrygritz.com>
1 parent c62e7ca commit b0de7d4

4 files changed

Lines changed: 157 additions & 126 deletions

File tree

src/fits.imageio/fitsinput.cpp

Lines changed: 143 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -210,142 +210,159 @@ FitsInput::close(void)
210210
bool
211211
FitsInput::read_fits_header(void)
212212
{
213-
std::string fits_header(HEADER_SIZE, 0);
214-
215-
// we read whole header at once
216-
if (fread(&fits_header[0], 1, HEADER_SIZE, m_fd) != HEADER_SIZE) {
217-
if (feof(m_fd))
218-
errorfmt("Hit end of file unexpectedly (offset={})", ftell(m_fd));
219-
else
220-
errorfmt("read error");
221-
return false; // Read failed
222-
}
223-
224-
bool found_end = false;
225-
for (int i = 0; i < CARDS_PER_HEADER; ++i) {
226-
std::string card(CARD_SIZE, 0);
227-
// reading card number i
228-
memcpy(&card[0], &fits_header[i * CARD_SIZE], CARD_SIZE);
229-
230-
std::string keyname, value;
231-
fits_pvt::unpack_card(card, keyname, value);
232-
233-
// END means that this is end of the FITS header
234-
// we can now add to the ImageSpec COMMENT, HISTORY and HIERARCH keys
235-
if (keyname == "END") {
236-
// removing white spaces that we use to separate lines of comments
237-
// from the end ot the string
238-
m_comment = m_comment.substr(0, m_comment.size() - m_sep.size());
239-
m_history = m_history.substr(0, m_history.size() - m_sep.size());
240-
m_hierarch = m_hierarch.substr(0, m_hierarch.size() - m_sep.size());
241-
add_to_spec("Comment", m_comment);
242-
add_to_spec("History", m_history);
243-
add_to_spec("Hierarch", m_hierarch);
244-
found_end = true;
245-
break;
213+
// Arbitrary limit to the number of header blocks in a file before we
214+
// conclude that it's a corrupted file. There are FITS files that *could*
215+
// have more headers, but there is no real limit, so we just pick one.
216+
// Somebody will let us know if they ever encounter a real world file that
217+
// intentionally exceeds this.
218+
constexpr int max_blocks = 10000;
219+
220+
for (int h = 0; h < max_blocks; ++h) {
221+
std::string fits_header(HEADER_SIZE, 0);
222+
223+
// we read whole header at once
224+
if (fread(&fits_header[0], 1, HEADER_SIZE, m_fd) != HEADER_SIZE) {
225+
if (feof(m_fd))
226+
errorfmt("Hit end of file unexpectedly (offset={})",
227+
ftell(m_fd));
228+
else
229+
errorfmt("read error");
230+
return false; // Read failed
246231
}
247232

248-
if (keyname == "SIMPLE" || keyname == "XTENSION")
249-
continue;
250-
251-
// setting up some important fields
252-
// m_bitpix - format of the data (eg. bpp)
253-
// m_naxes - number of axes
254-
// width, height and depth of the image
255-
if (keyname == "BITPIX") {
256-
m_bitpix = Strutil::stoi(&card[10]);
257-
continue;
258-
}
259-
if (keyname == "NAXIS") {
260-
m_naxes = Strutil::stoi(&card[10]);
261-
m_naxis.resize(m_naxes);
262-
continue;
263-
}
264-
if (Strutil::starts_with(keyname, "NAXIS")) {
265-
int i = Strutil::stoi(keyname.substr(5));
266-
if (i > 0 && i <= m_naxes)
267-
m_naxis[i - 1] = Strutil::stoi(&card[10]);
268-
continue;
269-
}
270-
if (keyname == "ORIENTAT") {
271-
add_to_spec("Orientation", value);
272-
continue;
233+
bool found_end = false;
234+
for (int i = 0; i < CARDS_PER_HEADER; ++i) {
235+
std::string card(CARD_SIZE, 0);
236+
// reading card number i
237+
memcpy(&card[0], &fits_header[i * CARD_SIZE], CARD_SIZE);
238+
239+
std::string keyname, value;
240+
fits_pvt::unpack_card(card, keyname, value);
241+
242+
// END means that this is end of the FITS header
243+
// we can now add to the ImageSpec COMMENT, HISTORY and HIERARCH keys
244+
if (keyname == "END") {
245+
// removing white spaces that we use to separate lines of comments
246+
// from the end ot the string
247+
m_comment = m_comment.substr(0,
248+
m_comment.size() - m_sep.size());
249+
m_history = m_history.substr(0,
250+
m_history.size() - m_sep.size());
251+
m_hierarch = m_hierarch.substr(0, m_hierarch.size()
252+
- m_sep.size());
253+
add_to_spec("Comment", m_comment);
254+
add_to_spec("History", m_history);
255+
add_to_spec("Hierarch", m_hierarch);
256+
found_end = true;
257+
break;
258+
}
259+
260+
if (keyname == "SIMPLE" || keyname == "XTENSION")
261+
continue;
262+
263+
// setting up some important fields
264+
// m_bitpix - format of the data (eg. bpp)
265+
// m_naxes - number of axes
266+
// width, height and depth of the image
267+
if (keyname == "BITPIX") {
268+
m_bitpix = Strutil::stoi(&card[10]);
269+
continue;
270+
}
271+
if (keyname == "NAXIS") {
272+
m_naxes = Strutil::stoi(&card[10]);
273+
m_naxis.resize(m_naxes);
274+
continue;
275+
}
276+
if (Strutil::starts_with(keyname, "NAXIS")) {
277+
int i = Strutil::stoi(keyname.substr(5));
278+
if (i > 0 && i <= m_naxes)
279+
m_naxis[i - 1] = Strutil::stoi(&card[10]);
280+
continue;
281+
}
282+
if (keyname == "ORIENTAT") {
283+
add_to_spec("Orientation", value);
284+
continue;
285+
}
286+
if (keyname == "DATE") {
287+
add_to_spec("DateTime", convert_date(value));
288+
continue;
289+
}
290+
if (keyname == "COMMENT") {
291+
m_comment += (value + m_sep);
292+
continue;
293+
}
294+
if (keyname == "HISTORY") {
295+
m_history += (value + m_sep);
296+
continue;
297+
}
298+
if (keyname == "HIERARCH") {
299+
m_hierarch += (value + m_sep);
300+
continue;
301+
}
302+
303+
Strutil::to_lower(keyname); // make lower case
304+
if (keyname.size() >= 1)
305+
keyname[0] = toupper(keyname[0]);
306+
add_to_spec(keyname, value);
273307
}
274-
if (keyname == "DATE") {
275-
add_to_spec("DateTime", convert_date(value));
276-
continue;
277-
}
278-
if (keyname == "COMMENT") {
279-
m_comment += (value + m_sep);
280-
continue;
308+
309+
// Fix up dimensions
310+
while (m_naxes > 1 && m_naxis[m_naxes - 1] == 1) {
311+
--m_naxes;
281312
}
282-
if (keyname == "HISTORY") {
283-
m_history += (value + m_sep);
284-
continue;
313+
if (m_naxes < 0 || m_naxes > 4) {
314+
errorfmt("Number of data axes {} not supported", m_naxes);
315+
return false;
285316
}
286-
if (keyname == "HIERARCH") {
287-
m_hierarch += (value + m_sep);
288-
continue;
317+
m_spec.nchannels = 1;
318+
m_spec.depth = 1;
319+
if (m_naxes == 0 || m_naxis[0] == 0) {
320+
m_spec.width = m_spec.height = 0;
321+
} else if (m_naxes == 1) {
322+
m_spec.width = m_naxis[0];
323+
m_spec.height = 1;
324+
} else if (m_naxes == 2) {
325+
m_spec.width = m_naxis[0];
326+
m_spec.height = m_naxis[1];
327+
} else if (m_naxes == 3 && m_naxis[0] <= 4) {
328+
// 3D, small number of most-rapidly changing dimension: color image?
329+
m_spec.nchannels = m_naxis[0];
330+
m_spec.width = m_naxis[1];
331+
m_spec.height = m_naxis[2];
332+
} else if (m_naxes == 3) {
333+
// 3D, large number of most-rapidly changing dimension: volume?
334+
m_spec.width = m_naxis[0];
335+
m_spec.height = m_naxis[1];
336+
m_spec.depth = m_naxis[2];
337+
} else if (m_naxes == 4) {
338+
// 4D... volume + color?
339+
m_spec.nchannels = m_naxis[0];
340+
m_spec.width = m_naxis[1];
341+
m_spec.height = m_naxis[2];
342+
m_spec.depth = m_naxis[3];
343+
} else {
344+
errorfmt("Don't know now to read {}-channel FITS image", m_naxes);
345+
return false;
289346
}
347+
m_spec.full_width = m_spec.width;
348+
m_spec.full_height = m_spec.height;
349+
m_spec.full_depth = m_spec.depth;
290350

291-
Strutil::to_lower(keyname); // make lower case
292-
if (keyname.size() >= 1)
293-
keyname[0] = toupper(keyname[0]);
294-
add_to_spec(keyname, value);
295-
}
296-
297-
// Fix up dimensions
298-
while (m_naxes > 1 && m_naxis[m_naxes - 1] == 1) {
299-
--m_naxes;
300-
}
301-
if (m_naxes < 0 || m_naxes > 4) {
302-
errorfmt("Number of data axes {} not supported", m_naxes);
303-
return false;
304-
}
305-
m_spec.nchannels = 1;
306-
m_spec.depth = 1;
307-
if (m_naxes == 0 || m_naxis[0] == 0) {
308-
m_spec.width = m_spec.height = 0;
309-
} else if (m_naxes == 1) {
310-
m_spec.width = m_naxis[0];
311-
m_spec.height = 1;
312-
} else if (m_naxes == 2) {
313-
m_spec.width = m_naxis[0];
314-
m_spec.height = m_naxis[1];
315-
} else if (m_naxes == 3 && m_naxis[0] <= 4) {
316-
// 3D, small number of most-rapidly changing dimension: color image?
317-
m_spec.nchannels = m_naxis[0];
318-
m_spec.width = m_naxis[1];
319-
m_spec.height = m_naxis[2];
320-
} else if (m_naxes == 3) {
321-
// 3D, large number of most-rapidly changing dimension: volume?
322-
m_spec.width = m_naxis[0];
323-
m_spec.height = m_naxis[1];
324-
m_spec.depth = m_naxis[2];
325-
} else if (m_naxes == 4) {
326-
// 4D... volume + color?
327-
m_spec.nchannels = m_naxis[0];
328-
m_spec.width = m_naxis[1];
329-
m_spec.height = m_naxis[2];
330-
m_spec.depth = m_naxis[3];
331-
} else {
332-
errorfmt("Don't know now to read {}-channel FITS image", m_naxes);
333-
return false;
334-
}
335-
m_spec.full_width = m_spec.width;
336-
m_spec.full_height = m_spec.height;
337-
m_spec.full_depth = m_spec.depth;
351+
m_spec.attribute("oiio:subimages", (int)m_subimages.size());
338352

339-
m_spec.attribute("oiio:subimages", (int)m_subimages.size());
353+
// if (m_spec.width < 1 || m_spec.height < 1 || m_spec.depth < 1 ||
354+
// m_spec.nchannels < 1) {
355+
// errorfmt("Don't know now to read empty (0 pixel) FITS image");
356+
// return false;
357+
// }
340358

341-
// if (m_spec.width < 1 || m_spec.height < 1 || m_spec.depth < 1 ||
342-
// m_spec.nchannels < 1) {
343-
// errorfmt("Don't know now to read empty (0 pixel) FITS image");
344-
// return false;
345-
// }
359+
// if we didn't found END keyword in current header, we read next one
360+
if (found_end)
361+
return true;
362+
}
346363

347-
// if we didn't found END keyword in current header, we read next one
348-
return found_end ? true : read_fits_header();
364+
errorfmt("No END keyword found after reading {} header blocks", max_blocks);
365+
return false; // Never found the end
349366
}
350367

351368

testsuite/fits/ref/out.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,3 +712,6 @@ AIPS CLEAN NITER= 12000 PRODUCT=1"
712712
oiio:subimages: 1
713713
Comparing "../fits-images/ftt4b/file012.fits" and "file012.fits"
714714
PASS
715+
oiiotool ERROR: read : "src/broken_no_END.fits": Hit end of file unexpectedly (offset=5760)
716+
Full command line was:
717+
> oiiotool --info --hash src/broken_no_END.fits

testsuite/fits/run.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# SPDX-License-Identifier: Apache-2.0
55
# https://github.com/AcademySoftwareFoundation/OpenImageIO
66

7+
# save the error output
8+
redirect = " >> out.txt 2>&1 "
9+
10+
711
# ../fits-image/pg93:
812
# tst0001.fits to tst0014.fits
913
imagedir = OIIO_TESTSUITE_IMAGEDIR + "/pg93"
@@ -20,3 +24,9 @@
2024
"file009.fits", "file012.fits" ]
2125
for f in files :
2226
command += rw_command (imagedir, f)
27+
28+
29+
# Regression tests for broken files
30+
command += info_command ("src/broken_no_END.fits", verbose=False, failureok=True)
31+
32+
outputs = [ "out.txt" ]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SIMPLE = T / Standard FITS BITPIX = 8 / 8-bit data NAXIS = 2 / Number of axes NAXIS1 = 1 / Width NAXIS2 = 1 / Height

0 commit comments

Comments
 (0)