Compare commits

...
14 Commits
Author SHA1 Message Date
Clifford Yapp ed686ee1d9 Merge pull request #487 from starseeker/develop
Address compiler warnings
2026-09-10 09:56:05 -04:00
Clifford Yapp 79926118c5 Merge pull request #486 from attixray/sc-fix-uninitialized-member-reads
Initialise two members that are read before they are written
2026-09-10 09:37:15 -04:00
Clifford Yapp 0f8c5eed76 Merge pull request #485 from attixray/sc-fix-header-scan-hang
Stop FindHeaderSection spinning forever on a file with no header
2026-09-10 09:36:30 -04:00
Clifford Yapp 9d47461e32 Merge pull request #484 from attixray/sc-fix-lazy-null-and-uninitialised-reads
lazy: refuse an instance whose data section failed to index, and three uninitialised reads around it
2026-09-10 09:35:56 -04:00
Clifford Yapp e480b0bbd1 Address compiler warnings 2026-09-10 13:22:16 +00:00
Attila Prokai 40bef08602 Terminate TypeDescriptor::_altname on construction
_altname is a char[BUFSIZ+1] that neither constructor touches, so CurrName()
and the AltNameOf paths can read uninitialised heap until something calls
AltName(). Start it empty.
2026-08-21 19:50:43 +02:00
Attila Prokai ee5c3dbfd2 Stop FindHeaderSection spinning on a file with no header
getline(buf, BUFSIZ, ';') on a stream whose next BUFSIZ bytes contain no
semicolon sets failbit and leaves buf unchanged. The scan loop only tests
eof(), which never arrives once failbit is set, so it never terminates - hand
it any binary file and it hangs instead of reporting that the file is not
STEP. Treat a failed extraction the same as end of file.
2026-08-21 19:50:43 +02:00
Attila Prokai 2e0b644c99 Initialise _null in aggregate nodes
STEPnode::_null had no default: STEPnode has no constructor, so a node built
by a subclass that does not assign it starts out with whatever was on the
heap, and is_null() then answers at random. Give STEPnode a constructor that
starts nodes null.

EntityNode::StrToVal() and ::STEPread() additionally have to set it per
outcome - they resolve an entity reference and must report null exactly when
the reference did not resolve. Before this, a node whose reference failed to
resolve could still report itself non-null.
2026-08-21 19:50:43 +02:00
Attila Prokai 18f7b43c99 lazy: refuse an instance whose data section failed to index
A data section indexes its instances from its own constructor, through
addLazyInstance(), under the section id it WILL be given - and
lazyFileReader::initP21() registers the section only afterwards, and only
if its scan succeeded. A section that fails part way therefore leaves
every instance it had already indexed naming a section that does not
exist.

typeFromFile() then indexes _dataSections with it unconditionally, and
loadInstance() does the same behind an assert() that Release compiles
out. On an empty vector that is an access violation. sourceRecord()
already makes exactly this test; the other two now make it too and answer
0 with a diagnostic, as they already do for an instance found in no
section.

The instances stay in the index - totalInstanceCount() still counts them
and getInstances() still lists them. What changes is that asking for one
of them fails the way every other unloadable instance fails.

Reachable from a thirteen-line file, added as test/lazy_broken_section.stp
and checked by the lazy_index test: a DATA section holding one instance
and then a line that is not one. Without the two changes below it, that
test dies with SIGSEGV inside typeFromFile().

Note that the new break in loadInstance() leaves `inst` null, and
isNilSTEPentity( 0 ) is false, so the null would be cached as a loaded
instance were it not for the guard added in "lazy: do not cache a null
instance as a loaded one" earlier in this branch. With the bounds test
alone the process survives the two calls and then dies on the way out.
2026-08-21 19:50:42 +02:00
Attila Prokai b5e0082f56 lazy: null the reference list the invalid-instance path deletes
nextInstance()'s "invalid instance, so clear everything" branch deletes
i.refs but leaves the pointer, while componentTypes two lines below is
deleted and nulled, and i.name is assigned 0 right after. The struct is
returned by value with the stale pointer still in it.

Nothing frees it twice today: the same branch zeroes loc.begin, and both
callers - the data section constructor's scan loop and indexScope() -
stop on that before looking at anything else. So this is the same kind of
uniformity fix as the two commits before it rather than a live bug, and
it costs one line to stop being a question.
2026-08-21 19:50:42 +02:00
Attila Prokai c4d4373280 lazy: initialise namedLazyInstance::name with the rest of the struct
nextInstance() explicitly initialises every member of the local
namedLazyInstance except `name`, and the assignment it does get is conditional:

    if( _file.good() ) i.name = getDelimitedKeyword( ";( /\\" );

Nothing is wrong today. When that guard is false the closing "invalid instance"
branch is reached on the same condition and assigns `i.name = 0` before the
struct is returned, so the uninitialised value is never observed - but that is a
control-flow argument spanning forty lines, not an invariant the code states.

It also stops holding the moment getDelimitedKeyword() can fail softly. It
cannot upstream, because it abort()s on a keyword with no delimiter after it; a
caller that would rather report bad input than terminate the host process has to
make it return 0, and then `i.name` is read while uninitialised.

One line, and the struct is uniformly initialised where it is declared.
2026-08-21 19:50:42 +02:00
Attila Prokai 54c89ed13e lazy: do not cache a null instance as a loaded one
isNilSTEPentity( 0 ) returns false, so `if( !isNilSTEPentity( inst ) )` in
loadInstance() is TRUE for a null pointer, and every branch of the switch above
it can leave one:

  * case 0 and default leave `inst` at the value returned by
    _instancesLoaded.find(), which is 0 on a cache miss;
  * case 1 sets `inst = 0` explicitly on the cancellation path added for batch
    loading.

A null therefore reaches the success arm and is inserted into _instancesLoaded,
while _loadedInstanceCount, _materializations, _cacheHighWater and the resident
source-byte accounting are all advanced for an instance that was never
materialized. The lazyRefs construction that follows is harmless - its init()
rejects two null arguments - so this is accounting corruption rather than a
crash.

It does not settle either. releaseBatch() skips the entry with `if( !inst )
continue;` BEFORE the `--_loadedInstanceCount` and the erase from
_batchOwnedInstances, so the phantom can never be evicted: the counter stays
high for the life of the manager and _batchOwnedInstances grows across batches.
cacheStatistics() reports the drift.

Guard the pointer as well as the nil sentinel. A null then takes the existing
else arm, which already emits LAZY_DIAGNOSTIC_ERROR "SDAI instance
materialization failed" - the right diagnostic for what happened.
2026-08-21 19:50:42 +02:00
Clifford Yapp 9baa5dadaa Merge pull request #482 from attixray/sc-fix-static-lib-link-interface
Fix the static library build: a generator expression leaks into the link line
2026-08-21 11:20:34 -04:00
Attila Prokai e7c70802d4 cldai: name the static dependencies instead of composing them
"cmake: declare target link interfaces explicitly" (0b950dd1) made SC_ADDLIB
publish its LINK_LIBRARIES as PUBLIC. That is the right change, but it is not
compatible with the way src/cldai/CMakeLists.txt names its static dependency:

    SC_ADDLIB(stepdai-static STATIC SOURCES ${DAI_SRCS}
              LINK_LIBRARIES $<JOIN:${_libdeps},-static >-static)

_libdeps holds one entry, steputils, so the generator expression is an elaborate
way of writing steputils-static. While the dependency was linked privately it
expanded once, on the link line of stepdai-static, and was correct. Published as
PUBLIC it lands in INTERFACE_LINK_LIBRARIES and is evaluated again in every
consumer, where it comes back out mangled and reaches the linker verbatim:

    LINK : fatal error LNK1104: cannot open file $<JOIN:steputils,-static.obj

p21read_sdai_<schema> is the first consumer to hit it, so with BUILD_STATIC_LIBS=ON
no schema executable links. Reproduced on develop tip - nothing after 0b950dd1
touches either file, so the static build is broken there too.

Building the list with a foreach costs nothing, reads the same, and keeps working
if _libdeps ever grows. The shared branch is untouched; this is inside
if(BUILD_STATIC_LIBS).
2026-08-19 13:16:28 +02:00
13 changed files with 119 additions and 18 deletions
+8
View File
@@ -289,6 +289,14 @@ class SC_LAZYFILE_EXPORT lazyInstMgr {
return 0;
}
instancePosition pos = cv->at( 0 );
//a data section indexes its instances from its own constructor and is
//registered only afterwards, and only if it succeeded - so an instance
//can name a section that was never registered. Same test sourceRecord()
//already makes.
if( pos.section >= _dataSections.size() || !_dataSections[pos.section] ) {
std::cerr << "Error at " << __FILE__ << ":" << __LINE__ << " - instanceID " << id << " names data section " << pos.section << ", which failed to index." << std::endl;
return 0;
}
return _dataSections[pos.section]->getType( pos.begin );
}
std::cerr << "Error at " << __FILE__ << ":" << __LINE__ << " - instanceID " << id << " not found." << std::endl;
+5
View File
@@ -88,6 +88,11 @@ class SC_CORE_EXPORT STEPnode : public SingleLinkNode {
protected:
int _null;
/// nodes must start out null; several StrToVal/STEPread paths leave
/// _null untouched on failure and would otherwise read uninitialised.
STEPnode() : _null( 1 ) {
}
public:
int is_null() {
return _null;
+10 -1
View File
@@ -27,7 +27,16 @@ if(BUILD_SHARED_LIBS)
endif()
if(BUILD_STATIC_LIBS)
SC_ADDLIB(stepdai-static STATIC SOURCES ${DAI_SRCS} LINK_LIBRARIES $<JOIN:${_libdeps},-static >-static)
# Spell the static dependency names out rather than composing them with a
# generator expression. Since 0b950dd1 SC_ADDLIB publishes LINK_LIBRARIES as
# PUBLIC, so anything given here lands in INTERFACE_LINK_LIBRARIES and is
# re-evaluated in each consumer - where a nested $<JOIN:...> comes back out
# mangled and reaches the linker verbatim.
set(_static_libdeps)
foreach(_libdep ${_libdeps})
list(APPEND _static_libdeps ${_libdep}-static)
endforeach()
SC_ADDLIB(stepdai-static STATIC SOURCES ${DAI_SRCS} LINK_LIBRARIES ${_static_libdeps})
endif()
# Local Variables:
+9
View File
@@ -743,6 +743,15 @@ int STEPfile::FindHeaderSection( istream & in ) {
return 0;
}
in.getline( buf, BUFSIZ, ';' ); // reads but does not store the ;
if( in.fail() ) {
// A file with no ';' in its first BUFSIZ bytes - a binary file that
// is not STEP at all, say - sets failbit and leaves buf unchanged.
// eof() then never becomes true and the loop above spins forever.
_error.AppendToUserMsg(
"Error: Unable to find HEADER section. File not read.\n" );
_error.GreaterSeverity( SEVERITY_INPUT_ERROR );
return 0;
}
}
return 1;
}
+2 -1
View File
@@ -55,5 +55,6 @@ if(SC_ENABLE_TESTING)
add_test(NAME lazy_index
COMMAND lazy_index_test
${CMAKE_CURRENT_SOURCE_DIR}/test/lazy_index.stp
${CMAKE_CURRENT_SOURCE_DIR}/test/lazy_scope.stp)
${CMAKE_CURRENT_SOURCE_DIR}/test/lazy_scope.stp
${CMAKE_CURRENT_SOURCE_DIR}/test/lazy_broken_section.stp)
endif()
+21 -2
View File
@@ -335,7 +335,24 @@ SDAI_Application_instance * lazyInstMgr::loadInstance( instanceID id, bool reSee
break;
case 1:
pos = cv->at( 0 );
assert( _dataSections.size() > pos.section );
//not an assert: a section that failed part way through its scan has
//already indexed the instances before the error under its id, and is
//then never registered. Those entries are bad input, not a programming
//error, and in Release the assert was compiled out and this indexed an
//empty vector. Same test sourceRecord() already makes.
if( pos.section >= _dataSections.size() || !_dataSections[pos.section] ) {
if( !_diagnosticCallback ) {
std::cerr << "Instance #" << id << " names data section "
<< pos.section << ", which failed to index." << std::endl;
}
LazyDiagnostic diagnostic;
diagnostic.severity = LAZY_DIAGNOSTIC_ERROR;
diagnostic.entity = id;
diagnostic.offset = pos.begin;
diagnostic.message = "instance belongs to a DATA section that failed to index";
emitDiagnostic( diagnostic );
break;
}
if( reSeek ) {
oldPos = _dataSections[pos.section]->tellg();
}
@@ -372,7 +389,9 @@ SDAI_Application_instance * lazyInstMgr::loadInstance( instanceID id, bool reSee
* safely revisit this instance without tripping the materialization
* cycle detector. */
loading_guard.release();
if( !isNilSTEPentity( inst ) ) {
/* isNilSTEPentity( 0 ) is false, so a null has to be tested for
* separately - every branch of the switch above can leave one. */
if( inst && !isNilSTEPentity( inst ) ) {
_instancesLoaded.insert( id, inst );
_loadedInstanceCount++;
++_materializations;
@@ -95,6 +95,7 @@ const namedLazyInstance lazyP21DataSectionReader::nextInstance() {
std::streampos end = -1;
namedLazyInstance i;
i.name = 0;
i.refs = 0;
i.componentTypes = 0;
i.loc.section = 0;
@@ -129,6 +130,7 @@ const namedLazyInstance lazyP21DataSectionReader::nextInstance() {
i.loc.begin = 0;
if( i.refs ) {
delete i.refs;
i.refs = 0;
}
if( i.componentTypes ) {
delete i.componentTypes;
+27 -1
View File
@@ -17,7 +17,7 @@ void emptyRegistryInit( Registry & ) {
}
int main( int argc, char ** argv ) {
require( argc == 3, "expected ordinary and scoped fixture paths" );
require( argc == 4, "expected ordinary, scoped and broken-section fixture paths" );
lazyInstMgr manager;
uint64_t progressCalls = 0;
@@ -133,5 +133,31 @@ int main( int argc, char ** argv ) {
complexOwner.find( "ENDSCOPE (C()D(" ) != std::string::npos &&
!complexOwner.empty() && complexOwner[complexOwner.size() - 1] == ';',
"complex scope source record was not preserved" );
/* A DATA section indexes its instances from its own constructor, under the
* id it will be given, and is registered only afterwards and only if the
* scan succeeded. A section that fails part way through therefore leaves
* every instance it had already indexed naming a section that does not
* exist. Asking for one of those must fail like any other unloadable
* instance rather than index an empty vector. */
lazyInstMgr broken;
broken.setRegistry( &emptyRegistry );
uint64_t brokenDiagnostics = 0;
broken.setDiagnosticCallback( [&brokenDiagnostics]( const LazyDiagnostic & ) {
++brokenDiagnostics;
} );
broken.openFile( argv[3] );
stats = broken.cacheStatistics();
require( stats.dataSections == 0, "the failed data section was registered anyway" );
require( broken.totalInstanceCount() == 1,
"the instance indexed before the failure was dropped from the index" );
require( broken.typeFromFile( 1 ) == 0,
"type of an instance in an unregistered section was not refused" );
require( broken.loadInstance( 1 ) == 0,
"instance in an unregistered section was not refused" );
require( brokenDiagnostics > 0,
"refusing the instance produced no diagnostic" );
require( broken.sourceRecord( 1 ).empty(),
"source record of an instance in an unregistered section was not empty" );
return EXIT_SUCCESS;
}
@@ -0,0 +1,13 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('lazy broken section test'),'2;1');
FILE_NAME('lazy_broken_section.stp','2026-08-21T00:00:00',('STEPcode'),('STEPcode'),'','','');
FILE_SCHEMA(('LAZY_TEST_SCHEMA { 1 0 10303 999 }'));
ENDSEC;
/* The DATA section indexes #1 and then fails: no ENDSEC; follows. The
section is therefore never registered, while #1 is already in the
instance index naming it. */
DATA;
#1=A();
not a step instance
END-ISO-10303-21;
+6
View File
@@ -169,14 +169,17 @@ Severity EntityNode::StrToVal( const char * s, ErrorDescriptor * err,
ErrorDescriptor error;
if( EntityValidLevel( se, elem_type, &error ) == SEVERITY_NULL ) {
node = se;
_null = 0;
} else {
node = S_ENTITY_NULL;
err->AppendToDetailMsg( error.DetailMsg() );
err->AppendToUserMsg( error.UserMsg() );
err->GreaterSeverity( error.severity() );
_null = 1;
}
} else {
node = S_ENTITY_NULL;
_null = 1;
}
return err->severity();
}
@@ -203,14 +206,17 @@ Severity EntityNode::STEPread( istream & in, ErrorDescriptor * err,
ErrorDescriptor error;
if( EntityValidLevel( se, elem_type, &error ) == SEVERITY_NULL ) {
node = se;
_null = 0;
} else {
node = S_ENTITY_NULL;
err->AppendToDetailMsg( error.DetailMsg() );
err->AppendToUserMsg( error.UserMsg() );
err->GreaterSeverity( error.severity() );
_null = 1;
}
} else {
node = S_ENTITY_NULL;
_null = 1;
}
return err->severity();
}
+2
View File
@@ -3,6 +3,7 @@
TypeDescriptor::TypeDescriptor( )
: _name( 0 ), altNames( 0 ), _fundamentalType( UNKNOWN_TYPE ),
_originatingSchema( 0 ), _referentType( 0 ), _description( 0 ), _where_rules( 0 ) {
_altname[0] = '\0';
}
TypeDescriptor::TypeDescriptor
@@ -11,6 +12,7 @@ TypeDescriptor::TypeDescriptor
: _name( nm ), altNames( 0 ), _fundamentalType( ft ),
_originatingSchema( origSchema ), _referentType( 0 ), _description( d ),
_where_rules( 0 ) {
_altname[0] = '\0';
}
TypeDescriptor::~TypeDescriptor() {
+3 -2
View File
@@ -157,11 +157,12 @@ HASHcreate( unsigned count ) {
/*
** Allocate initial 'i' segments of buckets
*/
for( i = 0; i < count; i++ )
for( i = 0; i < count; i++ ) {
CALLOC( table->Directory[i], SEGMENT_SIZE, Element )
/*, "segment in HASHcreate");*/
}
table->SegmentCount = count;
table->SegmentCount = count;
table->maxp = MUL( count, SEGMENT_SIZE_SHIFT );
table->MinLoadFactor = 1;
table->MaxLoadFactor = MAX_LOAD_FACTOR;
+11 -11
View File
@@ -310,16 +310,16 @@ Refinement collect_refinements_from_conjunction( Expression expr, Scope scope )
}
/* Create a refinement */
Refinement ref = ( Refinement ) malloc( sizeof( struct Refinement_ ) );
if( !ref ) {
Refinement refinement = ( Refinement ) malloc( sizeof( struct Refinement_ ) );
if( !refinement ) {
/* Memory allocation failed */
return NULL;
}
ref->variable = var;
ref->refined_type = target_type;
ref->next = NULL;
refinement->variable = var;
refinement->refined_type = target_type;
refinement->next = NULL;
return ref;
return refinement;
}
/* Not an AND node and not a matching pattern - don't recurse further */
@@ -347,12 +347,12 @@ static Type lookup_refinement( Variable var ) {
return NULL;
}
Refinement ref = active_refinements->refinements;
while( ref ) {
if( ref->variable == var ) {
return ref->refined_type;
Refinement refinement = active_refinements->refinements;
while( refinement ) {
if( refinement->variable == var ) {
return refinement->refined_type;
}
ref = ref->next;
refinement = refinement->next;
}
return NULL;