Reject Duration strings carrying more than nanosecond precision (#30446)
TimeUtil::FromString accepted fractional seconds with more than nine digits and silently produced a wrong Duration. The scaling step computes Pow(10, 9 - nanos_part.length()); when the fraction is longer than nine digits the exponent is negative, and Pow() treats a negative exponent as zero and returns 1. The unscaled digits are then handed to set_nanos, which truncates them to 32 bits, and IsDurationValid is happy with the result.
Concretely, before the change:
TimeUtil::FromString("0.3294967297s", &d) -> true, d = { seconds: 0, nanos: -999999999 }
TimeUtil::FromString("1.0000000001s", &d) -> true, d = { seconds: 1, nanos: 1 }
The first case flips the sign of the parsed value outright, since 3294967297 truncated to int32 is -999999999. The second case silently moves a digit: 1.0000000001s becomes 1.000000001s. A caller that parses attacker supplied duration text gets a valid looking message with an arbitrary value.
The fix rejects any fraction longer than nine digits, the most a Duration can represent, matching the JSON mapping rule that caps fractional seconds at nine digits. Values with nine digits or fewer are unchanged.
Testing: added assertions to DurationStringFormat in src/google/protobuf/util/time_util_test.cc. On unpatched main the five new FromString calls all return true (the EXPECT_FALSE lines fail); with the fix the full TimeUtilTest suite passes, 14 tests locally.
Closes #30446
COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/30446 from ManoharPaturi:fix-timeutil-duration-precision 7cd00b90a52fead245e8cc7dead270dba1b08887
PiperOrigin-RevId: 1002869518
diff --git a/src/google/protobuf/util/time_util.cc b/src/google/protobuf/util/time_util.cc
index d615545..22f9d25 100644
--- a/src/google/protobuf/util/time_util.cc
+++ b/src/google/protobuf/util/time_util.cc
@@ -264,6 +264,13 @@
if (end != nanos_part.c_str() + nanos_part.length()) {
return false;
}
+ // Fractional digits beyond nano-second precision cannot be represented.
+ // With more than 9 digits the exponent below would be negative, which
+ // Pow() silently treats as 0, leaving the value unscaled before it is
+ // truncated to 32 bits (e.g. "0.3294967297s" parsed as -999999999ns).
+ if (nanos_part.length() > 9) {
+ return false;
+ }
nanos = nanos * Pow(10, static_cast<int>(9 - nanos_part.length()));
if (negative) {
// If a Duration is negative, both seconds and nanos should be negative.
diff --git a/src/google/protobuf/util/time_util_test.cc b/src/google/protobuf/util/time_util_test.cc
index 0b3528d..9ef74e5 100644
--- a/src/google/protobuf/util/time_util_test.cc
+++ b/src/google/protobuf/util/time_util_test.cc
@@ -72,6 +72,20 @@
TEST(TimeUtilTest, DurationStringFormat) {
Timestamp begin, end;
+
+ // Fractional digits beyond nano-second precision cannot be represented and
+ // must be rejected: the value used to be mis-scaled and then truncated to
+ // 32 bits, silently producing a wrong (often sign-flipped) Duration.
+ {
+ Duration duration;
+ EXPECT_FALSE(TimeUtil::FromString("0.3294967297s", &duration));
+ EXPECT_FALSE(TimeUtil::FromString("-0.3294967297s", &duration));
+ EXPECT_FALSE(TimeUtil::FromString("0.4294967296s", &duration));
+ EXPECT_FALSE(TimeUtil::FromString("1.0000000001s", &duration));
+ EXPECT_FALSE(TimeUtil::FromString("0.99999999999999999999s", &duration));
+ EXPECT_TRUE(TimeUtil::FromString("0.999999999s", &duration));
+ EXPECT_EQ(999999999, duration.nanos());
+ }
EXPECT_TRUE(TimeUtil::FromString("0001-01-01T00:00:00Z", &begin));
EXPECT_TRUE(TimeUtil::FromString("9999-12-31T23:59:59.999999999Z", &end));