Skip to content

Commit 4fffc3f

Browse files
authored
Fix silent integer-to-char narrowing on origin_t numeric members (#489)
* Fix silent integer-to-char narrowing on origin_t numeric members Assigning a uint64_t to sdp_parameters::origin_t::session_version silently selected utility::string_t::operator=(char), producing a one-character string from the low octet of the right-hand side. This manifests as a corrupted o= line after IS-05 PATCH-driven SDP regeneration, e.g. o=- 1779263096 \xfc IN IP4 ... where \xfc is the low byte of the NTP-seconds session-version. Introduce nmos::numeric_string, a thin wrapper around utility::string_t that exposes two operator= overloads (utility::string_t with valid_numeric_string validation, and uint64_t with to_string_t). Other integer-ish right-hand sides (signed int, char, bool, ...) chain to operator=(uint64_t) and therefore produce the same implicit-conversion diagnostics on integer assignment that a uint64_t member would produce. Default construction delegates to numeric_string(uint64_t{}) so that origin_t() yields "0" / "0" for session_id / session_version, matching the historic uint64_t behaviour and producing a well-formed o= line from a freshly-constructed origin_t. Retype origin_t::session_id and origin_t::session_version to numeric_string. The existing string-pair and uint64_t-pair constructors compile unchanged because the wrapper accepts both forms; the explicit to_string_t / valid_numeric_string calls in those constructor bodies are now unnecessary and removed. Two value_of call sites in make_session_description access .value explicitly because std::pair<string_t, web::json::details::value_init> cannot chain two user-defined conversions (numeric_string -> string_t -> value_init). Tighten valid_numeric_string to reject the empty string (previously accepted because std::all_of on an empty range is vacuously true), aligning the big-digits-converter validation with digits2jn which already rejects empty input via istream >> uint64_t. Upgrade std::isdigit / std::isalnum calls on utility::char_t in valid_numeric_string and cpprest::is_tchar to the locale-aware templated overload std::isdigit(c, std::locale::classic()), removing the latent UB when utility::char_t is wchar_t and c is outside the unsigned-char range; the classic locale gives the same C-style ASCII semantics across narrow and wide char. Add testNumericString covering default == "0", uint64_t across the full range, leading-zero preservation, empty + non-numeric rejection, and the historic NTP-seconds footgun via origin_t::session_version. Signed-off-by: Gareth Sylvester-Bradley <garethsb@nvidia.com> * Add numeric_string construction and assignment from string rvalue Signed-off-by: Gareth Sylvester-Bradley <garethsb@nvidia.com> --------- Signed-off-by: Gareth Sylvester-Bradley <garethsb@nvidia.com>
1 parent 3d89c34 commit 4fffc3f

4 files changed

Lines changed: 96 additions & 10 deletions

File tree

Development/cpprest/http_utils.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ namespace web
189189
inline bool is_tchar(utility::char_t c)
190190
{
191191
static const utility::string_t tchar_punct{ U("!#$%&'*+-.^_`|~") };
192-
return std::isalnum(c) || std::string::npos != tchar_punct.find(c);
192+
return std::isalnum(c, std::locale::classic()) || std::string::npos != tchar_punct.find(c);
193193
}
194194

195195
// "A sender SHOULD NOT generate a quoted-pair in a quoted-string except where necessary to quote DQUOTE and backslash"

Development/nmos/sdp_utils.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,8 @@ namespace nmos
480480
// See https://tools.ietf.org/html/rfc4566#section-5.2
481481
{ sdp::fields::origin, value_of({
482482
{ sdp::fields::user_name, sdp_params.origin.user_name },
483-
{ sdp::fields::session_id, sdp_params.origin.session_id },
484-
{ sdp::fields::session_version, sdp_params.origin.session_version },
483+
{ sdp::fields::session_id, sdp_params.origin.session_id.value },
484+
{ sdp::fields::session_version, sdp_params.origin.session_version.value },
485485
{ sdp::fields::network_type, sdp::network_types::internet.name },
486486
{ sdp::fields::address_type, details::get_address_type_multicast(origin_address).first.name },
487487
{ sdp::fields::unicast_address, origin_address }
@@ -1626,9 +1626,10 @@ namespace nmos
16261626
}
16271627

16281628
// Validate the numeric string
1629+
// i.e. a non-empty sequence of decimal digits (leading zeros allowed)
16291630
const utility::string_t& valid_numeric_string(const utility::string_t& s)
16301631
{
1631-
if (!std::all_of(s.begin(), s.end(), [](utility::char_t c) { return std::isdigit(c); }))
1632+
if (s.empty() || !std::all_of(s.begin(), s.end(), [](utility::char_t c) { return std::isdigit(c, std::locale::classic()); }))
16321633
{
16331634
throw std::invalid_argument("not a numeric string");
16341635
}

Development/nmos/sdp_utils.h

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,33 @@ namespace nmos
6767

6868
// Validate the numeric string
6969
const utility::string_t& valid_numeric_string(const utility::string_t& s);
70+
inline utility::string_t&& valid_numeric_string(utility::string_t&& s) { return valid_numeric_string(s), std::move(s); }
71+
72+
// A non-negative integer represented as a numeric string.
73+
// Behaves as a utility::string_t for read access. On assignment it
74+
// accepts either a validated numeric string (preserving any leading
75+
// zeros) or a uint64_t (rendered as a minimal-width decimal string),
76+
// and produces the same implicit-conversion diagnostics on integer
77+
// assignment that a uint64_t member would produce (e.g. -Wsign-conversion
78+
// when the right-hand side is int/int64_t/etc.).
79+
// Introduced to prevent the silent narrowing footgun that
80+
// utility::string_t::operator=(char) would otherwise allow on a plain
81+
// string member, e.g. for sdp_parameters::origin_t::session_version.
82+
struct numeric_string
83+
{
84+
utility::string_t value;
85+
86+
numeric_string() : numeric_string(uint64_t{}) {}
87+
numeric_string(const utility::string_t& s) : value(valid_numeric_string(s)) {}
88+
numeric_string(utility::string_t&& s) : value(valid_numeric_string(std::move(s))) {}
89+
numeric_string(uint64_t n) : value(utility::conversions::details::to_string_t(n)) {}
90+
91+
numeric_string& operator=(const utility::string_t& s) { return value = valid_numeric_string(s), *this; }
92+
numeric_string& operator=(utility::string_t&& s) { return value = valid_numeric_string(std::move(s)), *this; }
93+
numeric_string& operator=(uint64_t n) { return value = utility::conversions::details::to_string_t(n), *this; }
94+
95+
operator const utility::string_t&() const { return value; }
96+
};
7097

7198
// Format-specific types
7299

@@ -87,19 +114,19 @@ namespace nmos
87114
struct origin_t
88115
{
89116
utility::string_t user_name;
90-
utility::string_t session_id;
91-
utility::string_t session_version;
117+
numeric_string session_id;
118+
numeric_string session_version;
92119

93120
origin_t() {}
94121
origin_t(const utility::string_t& user_name, uint64_t session_id, uint64_t session_version)
95122
: user_name(user_name)
96-
, session_id(utility::conversions::details::to_string_t(session_id))
97-
, session_version(utility::conversions::details::to_string_t(session_version))
123+
, session_id(session_id)
124+
, session_version(session_version)
98125
{}
99126
origin_t(const utility::string_t& user_name, const utility::string_t& session_id, const utility::string_t& session_version)
100127
: user_name(user_name)
101-
, session_id(valid_numeric_string(session_id))
102-
, session_version(valid_numeric_string(session_version))
128+
, session_id(session_id)
129+
, session_version(session_version)
103130
{}
104131
origin_t(const utility::string_t& user_name, uint64_t session_id_version)
105132
: origin_t{ user_name, session_id_version, session_id_version }

Development/nmos/test/sdp_utils_test.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "nmos/media_type.h"
1111
#include "nmos/random.h"
1212
#include "nmos/test/sdp_test_utils.h"
13+
#include "sdp/ntp.h"
1314
#include "sdp/sdp.h"
1415

1516
////////////////////////////////////////////////////////////////////////////////////////////
@@ -218,6 +219,63 @@ BST_TEST_CASE(testValidateSdpParameters)
218219
}
219220
}
220221

222+
////////////////////////////////////////////////////////////////////////////////////////////
223+
BST_TEST_CASE(testNumericString)
224+
{
225+
// Default-constructed value matches uint64_t{}, i.e. the string "0",
226+
// so a default-constructed origin_t still produces a well-formed SDP o= line
227+
{
228+
nmos::numeric_string ns;
229+
BST_REQUIRE_EQUAL(U("0"), static_cast<const utility::string_t&>(ns));
230+
}
231+
232+
// Integer assignment produces the corresponding minimal-width decimal string.
233+
// Other integer-ish right-hand sides (signed int, char, bool, etc.) implicitly
234+
// convert to uint64_t (the only operator= overload that takes a number) and
235+
// therefore produce a decimal string in the same way; we don't enumerate them
236+
// here because that would trigger any implicit-conversion compiler warnings
237+
// (the same as a uint64_t member would produce).
238+
{
239+
nmos::numeric_string ns;
240+
ns = uint64_t{ 0 };
241+
BST_REQUIRE_EQUAL(U("0"), static_cast<const utility::string_t&>(ns));
242+
ns = uint64_t{ 1779263096 };
243+
BST_REQUIRE_EQUAL(U("1779263096"), static_cast<const utility::string_t&>(ns));
244+
ns = (std::numeric_limits<uint64_t>::max)();
245+
BST_REQUIRE_EQUAL(U("18446744073709551615"), static_cast<const utility::string_t&>(ns));
246+
}
247+
248+
// String assignment preserves any leading zeros from the input
249+
{
250+
nmos::numeric_string ns;
251+
ns = U("0001779263096");
252+
BST_REQUIRE_EQUAL(U("0001779263096"), static_cast<const utility::string_t&>(ns));
253+
}
254+
255+
// Empty and non-numeric strings are rejected
256+
{
257+
nmos::numeric_string ns;
258+
BST_REQUIRE_THROW(ns = U(""), std::invalid_argument);
259+
BST_REQUIRE_THROW(ns = U("abc"), std::invalid_argument);
260+
BST_REQUIRE_THROW(ns = U("12a"), std::invalid_argument);
261+
BST_REQUIRE_THROW(ns = U("-1"), std::invalid_argument);
262+
BST_REQUIRE_THROW(ns = U(" 1"), std::invalid_argument);
263+
BST_REQUIRE_THROW(ns = U("1.0"), std::invalid_argument);
264+
BST_REQUIRE_THROW(nmos::numeric_string{ U("") }, std::invalid_argument);
265+
BST_REQUIRE_THROW(nmos::numeric_string{ U("abc") }, std::invalid_argument);
266+
}
267+
268+
// The historic footgun: assigning an integer to an origin_t numeric
269+
// string member must produce a multi-digit decimal string
270+
{
271+
nmos::sdp_parameters::origin_t o;
272+
o.session_version = sdp::ntp_now() >> 32;
273+
const utility::string_t& sv = o.session_version;
274+
BST_REQUIRE(sv.size() > 1);
275+
BST_REQUIRE(std::all_of(sv.begin(), sv.end(), [](utility::char_t c) { return std::isdigit(c, std::locale::classic()); }));
276+
}
277+
}
278+
221279
////////////////////////////////////////////////////////////////////////////////////////////
222280
BST_TEST_CASE(testSdpParametersRoundtrip)
223281
{

0 commit comments

Comments
 (0)