Skip to content

Commit 3543e96

Browse files
committed
Disallow negative TimeValues (elastic#53913)
This commit causes negative TimeValues, other than -1 which is sometimes used as a sentinel value, to be rejected during parsing. Also introduces a hack to allow ILM to load policies which were written to the cluster state with a negative min_age, treating those values as 0, which should match the behavior of prior versions.
1 parent 4008af3 commit 3543e96

File tree

14 files changed

+76
-55
lines changed

14 files changed

+76
-55
lines changed

libs/core/src/main/java/org/elasticsearch/common/unit/TimeValue.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ public TimeValue(long millis) {
7474
}
7575

7676
public TimeValue(long duration, TimeUnit timeUnit) {
77+
if (duration < -1) {
78+
throw new IllegalArgumentException("duration cannot be negative, was given [" + duration + "]");
79+
}
7780
this.duration = duration;
7881
this.timeUnit = timeUnit;
7982
}
@@ -186,7 +189,7 @@ public double getDaysFrac() {
186189
* Returns a {@link String} representation of the current {@link TimeValue}.
187190
*
188191
* Note that this method might produce fractional time values (ex 1.6m) which cannot be
189-
* parsed by method like {@link TimeValue#parse(String, String, String)}.
192+
* parsed by method like {@link TimeValue#parse(String, String, String, String)}.
190193
*/
191194
@Override
192195
public String toString() {
@@ -278,20 +281,20 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
278281
}
279282
final String normalized = sValue.toLowerCase(Locale.ROOT).trim();
280283
if (normalized.endsWith("nanos")) {
281-
return new TimeValue(parse(sValue, normalized, "nanos"), TimeUnit.NANOSECONDS);
284+
return new TimeValue(parse(sValue, normalized, "nanos", settingName), TimeUnit.NANOSECONDS);
282285
} else if (normalized.endsWith("micros")) {
283-
return new TimeValue(parse(sValue, normalized, "micros"), TimeUnit.MICROSECONDS);
286+
return new TimeValue(parse(sValue, normalized, "micros", settingName), TimeUnit.MICROSECONDS);
284287
} else if (normalized.endsWith("ms")) {
285-
return new TimeValue(parse(sValue, normalized, "ms"), TimeUnit.MILLISECONDS);
288+
return new TimeValue(parse(sValue, normalized, "ms", settingName), TimeUnit.MILLISECONDS);
286289
} else if (normalized.endsWith("s")) {
287-
return new TimeValue(parse(sValue, normalized, "s"), TimeUnit.SECONDS);
290+
return new TimeValue(parse(sValue, normalized, "s", settingName), TimeUnit.SECONDS);
288291
} else if (sValue.endsWith("m")) {
289292
// parsing minutes should be case-sensitive as 'M' means "months", not "minutes"; this is the only special case.
290-
return new TimeValue(parse(sValue, normalized, "m"), TimeUnit.MINUTES);
293+
return new TimeValue(parse(sValue, normalized, "m", settingName), TimeUnit.MINUTES);
291294
} else if (normalized.endsWith("h")) {
292-
return new TimeValue(parse(sValue, normalized, "h"), TimeUnit.HOURS);
295+
return new TimeValue(parse(sValue, normalized, "h", settingName), TimeUnit.HOURS);
293296
} else if (normalized.endsWith("d")) {
294-
return new TimeValue(parse(sValue, normalized, "d"), TimeUnit.DAYS);
297+
return new TimeValue(parse(sValue, normalized, "d", settingName), TimeUnit.DAYS);
295298
} else if (normalized.matches("-0*1")) {
296299
return TimeValue.MINUS_ONE;
297300
} else if (normalized.matches("0+")) {
@@ -303,10 +306,16 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
303306
}
304307
}
305308

306-
private static long parse(final String initialInput, final String normalized, final String suffix) {
309+
private static long parse(final String initialInput, final String normalized, final String suffix, String settingName) {
307310
final String s = normalized.substring(0, normalized.length() - suffix.length()).trim();
308311
try {
309-
return Long.parseLong(s);
312+
final long value = Long.parseLong(s);
313+
if (value < -1) {
314+
// -1 is magic, but reject any other negative values
315+
throw new IllegalArgumentException("failed to parse setting [" + settingName + "] with value [" + initialInput +
316+
"] as a time value: negative durations are not supported");
317+
}
318+
return value;
310319
} catch (final NumberFormatException e) {
311320
try {
312321
@SuppressWarnings("unused") final double ignored = Double.parseDouble(s);

libs/core/src/test/java/org/elasticsearch/common/unit/TimeValueTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,26 @@ public void testConversionHashCode() {
218218
TimeValue secondValue = new TimeValue(firstValue.getSeconds(), TimeUnit.SECONDS);
219219
assertEquals(firstValue.hashCode(), secondValue.hashCode());
220220
}
221+
222+
public void testRejectsNegativeValuesDuringParsing() {
223+
final String settingName = "test-value";
224+
final long negativeValue = randomLongBetween(Long.MIN_VALUE, -2);
225+
final String negativeTimeValueString = Long.toString(negativeValue) + randomTimeUnit();
226+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
227+
() -> TimeValue.parseTimeValue(negativeTimeValueString, settingName));
228+
assertThat(ex.getMessage(),
229+
equalTo("failed to parse setting [" + settingName + "] with value [" + negativeTimeValueString +
230+
"] as a time value: negative durations are not supported"));
231+
}
232+
233+
public void testRejectsNegativeValuesAtCreation() {
234+
final long duration = randomLongBetween(Long.MIN_VALUE, -2);
235+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new TimeValue(duration, randomTimeUnitObject()));
236+
assertThat(ex.getMessage(), containsString("duration cannot be negative"));
237+
}
238+
239+
private TimeUnit randomTimeUnitObject() {
240+
return randomFrom(TimeUnit.NANOSECONDS, TimeUnit.MICROSECONDS, TimeUnit.MILLISECONDS, TimeUnit.SECONDS,
241+
TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS);
242+
}
221243
}

server/src/main/java/org/elasticsearch/index/shard/IndexingStats.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ public long getDeleteCount() {
120120
/**
121121
* The total amount of time spend on executing delete operations.
122122
*/
123-
public TimeValue getDeleteTime() { return new TimeValue(deleteTimeInMillis); }
123+
public TimeValue getDeleteTime() {
124+
return new TimeValue(deleteTimeInMillis);
125+
}
124126

125127
/**
126128
* Returns the currently in-flight delete operations

server/src/main/java/org/elasticsearch/monitor/jvm/JvmGcMonitorService.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
import org.elasticsearch.common.unit.ByteSizeValue;
2929
import org.elasticsearch.common.unit.TimeValue;
3030
import org.elasticsearch.monitor.jvm.JvmStats.GarbageCollector;
31-
import org.elasticsearch.threadpool.ThreadPool;
3231
import org.elasticsearch.threadpool.Scheduler.Cancellable;
32+
import org.elasticsearch.threadpool.ThreadPool;
3333
import org.elasticsearch.threadpool.ThreadPool.Names;
3434

3535
import java.util.HashMap;
@@ -165,13 +165,20 @@ public JvmGcMonitorService(Settings settings, ThreadPool threadPool) {
165165
}
166166

167167
private static TimeValue getValidThreshold(Settings settings, String key, String level) {
168-
TimeValue threshold = settings.getAsTime(level, null);
168+
final TimeValue threshold;
169+
170+
try {
171+
threshold = settings.getAsTime(level, null);
172+
} catch (RuntimeException ex) {
173+
final String settingValue = settings.get(level);
174+
throw new IllegalArgumentException("failed to parse setting [" + getThresholdName(key, level) + "] with value [" +
175+
settingValue + "] as a time value", ex);
176+
}
177+
169178
if (threshold == null) {
170179
throw new IllegalArgumentException("missing gc_threshold for [" + getThresholdName(key, level) + "]");
171180
}
172-
if (threshold.nanos() <= 0) {
173-
throw new IllegalArgumentException("invalid gc_threshold [" + threshold + "] for [" + getThresholdName(key, level) + "]");
174-
}
181+
175182
return threshold;
176183
}
177184

server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa
520520
if (verbose || endTime != 0) {
521521
builder.field(END_TIME, DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(endTime).atZone(ZoneOffset.UTC)));
522522
builder.field(END_TIME_IN_MILLIS, endTime);
523-
builder.humanReadableField(DURATION_IN_MILLIS, DURATION, new TimeValue(endTime - startTime));
523+
builder.humanReadableField(DURATION_IN_MILLIS, DURATION, new TimeValue(Math.max(0L, endTime - startTime)));
524524
}
525525
if (verbose || !shardFailures.isEmpty()) {
526526
builder.startArray(FAILURES);

server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ public void testDayRounding() {
104104
Rounding tzRounding = Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.forOffsetHours(timezoneOffset))
105105
.build();
106106
assertThat(tzRounding.round(0), equalTo(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()));
107-
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(0L - TimeValue
108-
.timeValueHours(timezoneOffset).millis()));
107+
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(TimeValue
108+
.timeValueHours(-timezoneOffset).millis()));
109109

110110
DateTimeZone tz = DateTimeZone.forID("-08:00");
111111
tzRounding = Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(tz).build();

server/src/test/java/org/elasticsearch/indices/IndexingMemoryControllerTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,17 @@ public void testNegativeInterval() {
260260
Exception e = expectThrows(IllegalArgumentException.class,
261261
() -> new MockController(Settings.builder()
262262
.put("indices.memory.interval", "-42s").build()));
263-
assertEquals("failed to parse value [-42s] for setting [indices.memory.interval], must be >= [0ms]", e.getMessage());
263+
assertEquals("failed to parse setting [indices.memory.interval] with value " +
264+
"[-42s] as a time value: negative durations are not supported", e.getMessage());
264265

265266
}
266267

267268
public void testNegativeShardInactiveTime() {
268269
Exception e = expectThrows(IllegalArgumentException.class,
269270
() -> new MockController(Settings.builder()
270271
.put("indices.memory.shard_inactive_time", "-42s").build()));
271-
assertEquals("failed to parse value [-42s] for setting [indices.memory.shard_inactive_time], must be >= [0ms]", e.getMessage());
272+
assertEquals("failed to parse setting [indices.memory.shard_inactive_time] with value " +
273+
"[-42s] as a time value: negative durations are not supported", e.getMessage());
272274

273275
}
274276

server/src/test/java/org/elasticsearch/monitor/jvm/JvmGcMonitorServiceSettingsTests.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import java.util.concurrent.atomic.AtomicBoolean;
3434
import java.util.function.Consumer;
3535

36-
import static org.hamcrest.CoreMatchers.allOf;
3736
import static org.hamcrest.CoreMatchers.containsString;
37+
import static org.hamcrest.CoreMatchers.equalTo;
3838
import static org.hamcrest.CoreMatchers.instanceOf;
3939

4040
public class JvmGcMonitorServiceSettingsTests extends ESTestCase {
@@ -62,11 +62,12 @@ public void testDisabledSetting() throws InterruptedException {
6262

6363
public void testNegativeSetting() throws InterruptedException {
6464
String collector = randomAlphaOfLength(5);
65-
Settings settings = Settings.builder().put("monitor.jvm.gc.collector." + collector + ".warn", "-" + randomTimeValue()).build();
65+
final String timeValue = "-" + randomTimeValue();
66+
Settings settings = Settings.builder().put("monitor.jvm.gc.collector." + collector + ".warn", timeValue).build();
6667
execute(settings, (command, interval, name) -> null, e -> {
6768
assertThat(e, instanceOf(IllegalArgumentException.class));
68-
assertThat(e.getMessage(), allOf(containsString("invalid gc_threshold"),
69-
containsString("for [monitor.jvm.gc.collector." + collector + ".")));
69+
assertThat(e.getMessage(), equalTo("failed to parse setting [monitor.jvm.gc.collector." + collector + ".warn] " +
70+
"with value [" + timeValue + "] as a time value"));
7071
}, true, null);
7172
}
7273

x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ protected ShardFollowNodeTaskStatus createTestInstance() {
6060
randomNonNegativeLong(),
6161
randomNonNegativeLong(),
6262
randomReadExceptions(),
63-
randomLong(),
63+
randomNonNegativeLong(),
6464
randomBoolean() ? new ElasticsearchException("fatal error") : null);
6565
}
6666

x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/StatsResponsesTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ static FollowStatsAction.StatsResponses createStatsResponse() {
5757
randomNonNegativeLong(),
5858
randomNonNegativeLong(),
5959
Collections.emptyNavigableMap(),
60-
randomLong(),
60+
randomNonNegativeLong(),
6161
randomBoolean() ? new ElasticsearchException("fatal error") : null);
6262
responses.add(new FollowStatsAction.StatsResponse(status));
6363
}

0 commit comments

Comments
 (0)