#14901: Guard against crashes from corrupt Eclipse files and incomplete frustum

- RifEclipseOutputFileTools::timeSteps: avoid integer division by zero when
  the restart file has no INTEHEAD
- libecl ecl_grid: return NULL when the main EGRID grid cannot be created,
  and skip NNCHEAD blocks that cannot be read
- libecl well_info: skip wells when the restart header cannot be read
- cvf::Frustum::isOutside: do not assert on an incomplete frustum, cull nothing

Fixes #14901.
This commit is contained in:
Magne Sjaastad committed 2026-10-12 17:16:11 +02:00
1 parent 9722bfbaed
commit 92e269ef4d
6 files changed
+136 -8

No files matched your search

@@ -2,9 +2,25 @@
#include "cvfBoundingBox.h"
#include "cvfCamera.h"
#include "cvfFrustum.h"
using namespace cvf;
//--------------------------------------------------------------------------------------------------
/// Camera::computeViewFrustum() returns an empty frustum for a degenerate projection matrix. View
/// frustum culling in ModelBasicList::partVisible() then asserted in Frustum::isOutside().
//--------------------------------------------------------------------------------------------------
TEST( CvfCameraCrashTriageTest, IncompleteFrustumDoesNotCull )
{
Frustum frustum;
BoundingBox bb;
bb.add( Vec3d( 0, 0, 0 ) );
bb.add( Vec3d( 1, 1, 1 ) );
EXPECT_FALSE( frustum.isOutside( bb ) );
}
//--------------------------------------------------------------------------------------------------
/// computeFitViewEyePosition() used to assert/abort (CVF_ASSERT in Plane::distanceToOrigin) when
/// the view direction and up vector are parallel: dir^up then becomes a zero vector, producing an
@@ -23,8 +23,12 @@
#include "RigEclipseCaseData.h"
#include "ert/ecl/ecl_kw_magic.h"
#include <ert/ecl/ecl_endian_flip.h>
#include <ert/ecl/ecl_file.h>
#include <ert/ecl/ecl_grid.h>
#include <ert/ecl/ecl_kw.h>
#include <ert/ecl/fortio.h>
#include <ert/ecl_well/well_info.hpp>
#include "RiaStringEncodingTools.h"
#include "RiaTestDataDirectory.h"
@@ -207,6 +211,101 @@ TEST( RigReservoirTest, OpenGridFileWithoutDimensKeyword )
EXPECT_FALSE( readerInterfaceEcl->open( gridFileName, reservoir.p() ) );
}
TEST( RigReservoirTest, OpenEgridWithInvalidGridHeader )
{
// ecl_grid_alloc_EGRID__() returns NULL for invalid grid dimensions. The caller dereferenced the NULL main
// grid and crashed instead of returning NULL.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString gridFileName = QDir( tempDir.path() ).filePath( "TEST.EGRID" );
{
std::vector<int> intData( 100, 0 );
std::vector<float> floatData( 1, 0.0f );
fortio_type* fortio = fortio_open_writer( RiaStringEncodingTools::toNativeEncoded( gridFileName ).data(), false, ECL_ENDIAN_FLIP );
ASSERT_TRUE( fortio );
auto writeKeyword = [fortio]( const char* name, int size, ecl_data_type type, const void* data )
{
ecl_kw_type* kw = ecl_kw_alloc_new( name, size, type, data );
ecl_kw_fwrite( kw, fortio );
ecl_kw_free( kw );
};
// GRIDHEAD with zero grid dimensions is rejected
writeKeyword( "FILEHEAD", (int)intData.size(), ECL_INT, intData.data() );
writeKeyword( "GRIDHEAD", (int)intData.size(), ECL_INT, intData.data() );
writeKeyword( "COORD", (int)floatData.size(), ECL_FLOAT, floatData.data() );
writeKeyword( "ZCORN", (int)floatData.size(), ECL_FLOAT, floatData.data() );
fortio_fclose( fortio );
}
EXPECT_EQ( nullptr, ecl_grid_alloc( RiaStringEncodingTools::toNativeEncoded( gridFileName ).data() ) );
}
TEST( RigReservoirTest, ReadWellsFromRestartFileWithoutIntehead )
{
// ecl_rsthead_alloc() returns NULL when INTEHEAD is missing or unreadable (truncated file). The well reader
// dereferenced the NULL header and crashed.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString restartFileName = QDir( tempDir.path() ).filePath( "TEST.UNRST" );
{
std::vector<int> data( 1, 1 );
ecl_kw_fwrite_param( RiaStringEncodingTools::toNativeEncoded( restartFileName ).data(),
false,
"SEQNUM",
ECL_INT,
(int)data.size(),
data.data() );
}
ecl_file_type* ecl_file = ecl_file_open( RiaStringEncodingTools::toNativeEncoded( restartFileName ).data(), 0 );
ASSERT_TRUE( ecl_file );
well_info_type* wellInfo = well_info_alloc( { "GLOBAL" } );
well_info_add_UNRST_wells( wellInfo, ecl_file, false );
EXPECT_EQ( 0, well_info_get_num_wells( wellInfo ) );
well_info_free( wellInfo );
ecl_file_close( ecl_file );
}
TEST( RigReservoirTest, TimeStepsFromFileWithoutIntehead )
{
// A restart file without INTEHEAD gives no time steps, and computing the header keyword count per time step
// divided by zero.
QTemporaryDir tempDir;
ASSERT_TRUE( tempDir.isValid() );
QString restartFileName = QDir( tempDir.path() ).filePath( "TEST.UNRST" );
{
std::vector<int> data( 1, 1 );
ecl_kw_fwrite_param( RiaStringEncodingTools::toNativeEncoded( restartFileName ).data(),
false,
"SEQNUM",
ECL_INT,
(int)data.size(),
data.data() );
}
ecl_file_type* ecl_file = ecl_file_open( RiaStringEncodingTools::toNativeEncoded( restartFileName ).data(), 0 );
ASSERT_TRUE( ecl_file );
std::vector<QDateTime> timeSteps;
std::vector<double> daysSinceSimulationStart;
size_t perTimeStepHeaderKeywordCount = 0;
RifEclipseOutputFileTools::timeSteps( ecl_file, &timeSteps, &daysSinceSimulationStart, &perTimeStepHeaderKeywordCount );
EXPECT_TRUE( timeSteps.empty() );
EXPECT_EQ( size_t( 0 ), perTimeStepHeaderKeywordCount );
ecl_file_close( ecl_file );
}
TEST( RigReservoirTest, BasicTest10kRestart )
{
RifEclipseUnifiedRestartFileAccess unrstAccess;