mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Fix leak of mid-parse entities when an exception escapes the scan loop
read_from_stream() ran the entity-scanning while loop with no enclosing try/catch. If an exception escaped construction of an instance mid-stream (e.g. malformed attribute data following a syntactically valid header), it unwound straight out of IfcFile's constructor body, which has no try/catch of its own around this call. The constructor never completes, ~IfcFile() never runs its usual byid_ cleanup, and any entities already inserted into byid_ during this same scan are leaked with nothing left to free them. Catch the exception at the loop instead, matching how a bad header is already handled via tryRead(): mark the file INVALID_SYNTAX and stop scanning, so the constructor always completes and existing cleanup paths apply to whatever was parsed so far. Caught broadly (std::exception, not just IfcException): parsing helpers below this point also use plain std::out_of_range for malformed-input control flow (e.g. FileReader::peek() at EOF), not just IfcException, and any of those escaping here is exactly as leak-prone. Found by fuzzing (fuzzer-0f1); repro is a valid IFC4 header followed by '#1=IFCPERSON(wi);#1=IFCORGANIZATION,e'. Generated with the assistance of an AI coding tool.
This commit is contained in:
+47
-41
@@ -1623,57 +1623,63 @@ void IfcParse::impl::in_memory_file_storage::read_from_stream(IfcParse::FileRead
|
||||
logger().Status("Scanning file...");
|
||||
|
||||
while (streamer) {
|
||||
try {
|
||||
auto inst = streamer.readInstance();
|
||||
|
||||
auto inst = streamer.readInstance();
|
||||
if (!inst) {
|
||||
// No more instances to read
|
||||
break;
|
||||
}
|
||||
|
||||
if (!inst) {
|
||||
// No more instances to read
|
||||
break;
|
||||
}
|
||||
auto current_id = std::get<0>(*inst);
|
||||
|
||||
auto current_id = std::get<0>(*inst);
|
||||
auto instance = schema->instantiate(std::get<1>(*inst), std::move(std::get<2>(*inst)));
|
||||
instance->file_ = file;
|
||||
instance->id_ = (uint32_t) current_id;
|
||||
|
||||
auto instance = schema->instantiate(std::get<1>(*inst), std::move(std::get<2>(*inst)));
|
||||
instance->file_ = file;
|
||||
instance->id_ = (uint32_t) current_id;
|
||||
|
||||
if (instance->declaration().is(*ifcroot_type_)) {
|
||||
try {
|
||||
// @nb here we know we're using in-memory so 'nullptr, nullptr, 0' is safe
|
||||
const std::string guid = instance->data().get_attribute_value(nullptr, nullptr, 0, 0);
|
||||
if (byguid_.find(guid) != byguid_.end()) {
|
||||
std::stringstream ss;
|
||||
ss << "Instance encountered with non-unique GlobalId " << guid;
|
||||
logger().Message(Logger::LOG_WARNING, "SYN", 16, ss.str());
|
||||
if (instance->declaration().is(*ifcroot_type_)) {
|
||||
try {
|
||||
// @nb here we know we're using in-memory so 'nullptr, nullptr, 0' is safe
|
||||
const std::string guid = instance->data().get_attribute_value(nullptr, nullptr, 0, 0);
|
||||
if (byguid_.find(guid) != byguid_.end()) {
|
||||
std::stringstream ss;
|
||||
ss << "Instance encountered with non-unique GlobalId " << guid;
|
||||
logger().Message(Logger::LOG_WARNING, "SYN", 16, ss.str());
|
||||
}
|
||||
byguid_[guid] = instance;
|
||||
} catch (const IfcException& ex) {
|
||||
logger().Message(Logger::LOG_ERROR, "SYN", 17, ex.what());
|
||||
}
|
||||
byguid_[guid] = instance;
|
||||
} catch (const IfcException& ex) {
|
||||
logger().Message(Logger::LOG_ERROR, "SYN", 17, ex.what());
|
||||
}
|
||||
}
|
||||
|
||||
const IfcParse::declaration* ty = &instance->declaration();
|
||||
const IfcParse::declaration* ty = &instance->declaration();
|
||||
|
||||
{
|
||||
if (bytype_excl_.find(ty) == bytype_excl_.end()) {
|
||||
bytype_excl_[ty].reset(new aggregate_of_instance());
|
||||
{
|
||||
if (bytype_excl_.find(ty) == bytype_excl_.end()) {
|
||||
bytype_excl_[ty].reset(new aggregate_of_instance());
|
||||
}
|
||||
bytype_excl_[ty]->push(instance);
|
||||
}
|
||||
bytype_excl_[ty]->push(instance);
|
||||
|
||||
if (byid_.find(current_id) != byid_.end()) {
|
||||
std::stringstream ss;
|
||||
ss << "Overwriting instance with name #" << current_id;
|
||||
logger().Message(Logger::LOG_WARNING, "SYN", 18, ss.str());
|
||||
}
|
||||
|
||||
// byidentity_[instance->identity()] = instance;
|
||||
byid_.insert({(uint32_t) current_id, instance });
|
||||
|
||||
// @nb cannot assign to byid_;
|
||||
// byid_[current_id] = instance;
|
||||
|
||||
max_id = (std::max)(max_id, (unsigned int) current_id);
|
||||
} catch (const std::exception& e) {
|
||||
// Stop scanning (like a bad header already does via tryRead()) instead of letting this escape IfcFile's constructor, which has no try/catch of its own and would leak everything already in byid_.
|
||||
good_ = file_open_status::INVALID_SYNTAX;
|
||||
logger().Error("SYN", 42, e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (byid_.find(current_id) != byid_.end()) {
|
||||
std::stringstream ss;
|
||||
ss << "Overwriting instance with name #" << current_id;
|
||||
logger().Message(Logger::LOG_WARNING, "SYN", 18, ss.str());
|
||||
}
|
||||
|
||||
// byidentity_[instance->identity()] = instance;
|
||||
byid_.insert({(uint32_t) current_id, instance });
|
||||
|
||||
// @nb cannot assign to byid_;
|
||||
// byid_[current_id] = instance;
|
||||
|
||||
max_id = (std::max)(max_id, (unsigned int) current_id);
|
||||
}
|
||||
|
||||
good_ = streamer.status();
|
||||
|
||||
Reference in New Issue
Block a user