Skip to content

Commit b5891fc

Browse files
committed
P3-A1: harden compiled profile structural invariants
Reject hidden VLAN/SmvOpts state, embedded-NUL identifier smuggling, noncanonical leaf widths, and descriptor flag/type mismatches before binary encoding. Keep the envelope canonical so a future embedded decoder cannot reinterpret malformed but checksummed records. Tracks #36.
1 parent 3f810ab commit b5891fc

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎src/sampled_values/compiled_device_profile.cpp‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ void write_u32(
9292
raw <= static_cast<std::uint8_t>(SvDeviceWireType::timestamp);
9393
}
9494

95+
[[nodiscard]] std::optional<std::uint16_t> canonical_wire_width(
96+
const SvDeviceWireType type) noexcept {
97+
switch (type) {
98+
case SvDeviceWireType::boolean:
99+
case SvDeviceWireType::int8:
100+
case SvDeviceWireType::uint8:
101+
return static_cast<std::uint16_t>(1U);
102+
case SvDeviceWireType::int16:
103+
case SvDeviceWireType::uint16:
104+
return static_cast<std::uint16_t>(2U);
105+
case SvDeviceWireType::int32:
106+
case SvDeviceWireType::uint32:
107+
case SvDeviceWireType::float32:
108+
case SvDeviceWireType::quality:
109+
return static_cast<std::uint16_t>(4U);
110+
case SvDeviceWireType::int64:
111+
case SvDeviceWireType::uint64:
112+
case SvDeviceWireType::float64:
113+
case SvDeviceWireType::timestamp:
114+
return static_cast<std::uint16_t>(8U);
115+
}
116+
return std::nullopt;
117+
}
118+
95119
[[nodiscard]] bool valid_profile_shape(
96120
const CompiledSvDeviceProfile& profile) noexcept {
97121
if (profile.schema_version != compiled_sv_device_profile_version ||
@@ -112,6 +136,26 @@ void write_u32(
112136
profile.data_set_reference_length == 0U) {
113137
return false;
114138
}
139+
if (!profile.asdu_options.element_present &&
140+
(profile.asdu_options.refresh_time ||
141+
profile.asdu_options.sample_synchronized ||
142+
profile.asdu_options.sample_rate ||
143+
profile.asdu_options.data_set ||
144+
profile.asdu_options.security ||
145+
profile.asdu_options.synch_source_id)) {
146+
return false;
147+
}
148+
if (!profile.vlan_present &&
149+
(profile.vlan_id != 0U || profile.vlan_priority != 0U)) {
150+
return false;
151+
}
152+
const std::string_view sv_id{
153+
profile.sv_id.data(), profile.sv_id_length};
154+
const std::string_view data_set{
155+
profile.data_set_reference.data(), profile.data_set_reference_length};
156+
if (contains_nul(sv_id) || contains_nul(data_set)) {
157+
return false;
158+
}
115159
if (!is_valid_sv_app_id(profile.app_id)) return false;
116160
if (profile.transport_mode == SvTransportMode::multicast) {
117161
if (!is_iec_sv_multicast_mac(profile.destination_mac)) return false;
@@ -131,7 +175,11 @@ void write_u32(
131175
std::uint64_t payload{};
132176
for (std::size_t index = 0U; index < profile.leaf_count; ++index) {
133177
const auto& leaf = profile.leaves[index];
134-
if (!valid_wire_type(leaf.wire_type) || leaf.wire_width_bytes == 0U) {
178+
const auto expected_width = canonical_wire_width(leaf.wire_type);
179+
if (!expected_width.has_value() ||
180+
leaf.wire_width_bytes != *expected_width ||
181+
leaf.quality != (leaf.wire_type == SvDeviceWireType::quality) ||
182+
leaf.timestamp != (leaf.wire_type == SvDeviceWireType::timestamp)) {
135183
return false;
136184
}
137185
payload += leaf.wire_width_bytes;

‎tests/test_sampled_values.cpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,18 @@ void compiled_device_profile_binary_is_canonical_and_integrity_checked() {
336336
CHECK(short_result.status == SvDeviceProfileCodecStatus::buffer_too_small);
337337
CHECK(short_result.bytes == encoded.size());
338338

339+
auto invalid_width = profile;
340+
invalid_width.leaves[1].wire_width_bytes = 2U;
341+
CHECK(!SvDeviceProfileBinaryCodec::encoded_size(invalid_width).has_value());
342+
343+
auto embedded_nul = profile;
344+
embedded_nul.sv_id[1] = '\0';
345+
CHECK(!SvDeviceProfileBinaryCodec::encoded_size(embedded_nul).has_value());
346+
347+
auto inconsistent_options = profile;
348+
inconsistent_options.asdu_options.element_present = false;
349+
CHECK(!SvDeviceProfileBinaryCodec::encoded_size(inconsistent_options).has_value());
350+
339351
auto corrupted = encoded;
340352
corrupted.back() ^= 0x01U;
341353
CHECK(SvDeviceProfileBinaryCodec::decode(corrupted, decoded).status ==

0 commit comments

Comments
 (0)