Skip to content

Commit 85f6960

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 c466881 commit 85f6960

File tree

24 files changed

+124
-75
lines changed

24 files changed

+124
-75
lines changed

client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/CcrStatsResponseTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ static FollowStatsAction.StatsResponses createStatsResponse() {
109109
randomNonNegativeLong(),
110110
randomNonNegativeLong(),
111111
Collections.emptyNavigableMap(),
112-
randomLong(),
112+
randomNonNegativeLong(),
113113
randomBoolean() ? new ElasticsearchException("fatal error") : null);
114114
responses.add(new FollowStatsAction.StatsResponse(status));
115115
}

client/rest-high-level/src/test/java/org/elasticsearch/client/enrich/StatsResponseTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private static TaskInfo randomTaskInfo() {
8787
String action = randomAlphaOfLength(5);
8888
String description = randomAlphaOfLength(5);
8989
long startTime = randomLong();
90-
long runningTimeNanos = randomLong();
90+
long runningTimeNanos = randomNonNegativeLong();
9191
boolean cancellable = randomBoolean();
9292
TaskId parentTaskId = TaskId.EMPTY_TASK_ID;
9393
Map<String, String> headers = randomBoolean() ?

client/rest-high-level/src/test/java/org/elasticsearch/client/indexlifecycle/IndexLifecycleExplainResponseTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private static IndexLifecycleExplainResponse randomManagedIndexExplainResponse()
5757
boolean stepNull = randomBoolean();
5858
return IndexLifecycleExplainResponse.newManagedIndexResponse(randomAlphaOfLength(10),
5959
randomAlphaOfLength(10),
60-
randomBoolean() ? null : randomNonNegativeLong(),
60+
randomBoolean() ? null : randomLongBetween(0, System.currentTimeMillis()),
6161
stepNull ? null : randomAlphaOfLength(10),
6262
stepNull ? null : randomAlphaOfLength(10),
6363
stepNull ? null : randomAlphaOfLength(10),

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

+20-11
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public TimeValue(long millis) {
4747
}
4848

4949
public TimeValue(long duration, TimeUnit timeUnit) {
50+
if (duration < -1) {
51+
throw new IllegalArgumentException("duration cannot be negative, was given [" + duration + "]");
52+
}
5053
this.duration = duration;
5154
this.timeUnit = timeUnit;
5255
}
@@ -201,7 +204,7 @@ public double getDaysFrac() {
201204
* Returns a {@link String} representation of the current {@link TimeValue}.
202205
*
203206
* Note that this method might produce fractional time values (ex 1.6m) which cannot be
204-
* parsed by method like {@link TimeValue#parse(String, String, String)}.
207+
* parsed by method like {@link TimeValue#parse(String, String, String, String)}.
205208
*
206209
* Also note that the maximum string value that will be generated is
207210
* {@code 106751.9d} due to the way that values are internally converted
@@ -216,7 +219,7 @@ public String toString() {
216219
* Returns a {@link String} representation of the current {@link TimeValue}.
217220
*
218221
* Note that this method might produce fractional time values (ex 1.6m) which cannot be
219-
* parsed by method like {@link TimeValue#parse(String, String, String)}. The number of
222+
* parsed by method like {@link TimeValue#parse(String, String, String, String)}. The number of
220223
* fractional decimals (up to 10 maximum) are truncated to the number of fraction pieces
221224
* specified.
222225
*
@@ -358,20 +361,20 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
358361
}
359362
final String normalized = sValue.toLowerCase(Locale.ROOT).trim();
360363
if (normalized.endsWith("nanos")) {
361-
return new TimeValue(parse(sValue, normalized, "nanos"), TimeUnit.NANOSECONDS);
364+
return new TimeValue(parse(sValue, normalized, "nanos", settingName), TimeUnit.NANOSECONDS);
362365
} else if (normalized.endsWith("micros")) {
363-
return new TimeValue(parse(sValue, normalized, "micros"), TimeUnit.MICROSECONDS);
366+
return new TimeValue(parse(sValue, normalized, "micros", settingName), TimeUnit.MICROSECONDS);
364367
} else if (normalized.endsWith("ms")) {
365-
return new TimeValue(parse(sValue, normalized, "ms"), TimeUnit.MILLISECONDS);
368+
return new TimeValue(parse(sValue, normalized, "ms", settingName), TimeUnit.MILLISECONDS);
366369
} else if (normalized.endsWith("s")) {
367-
return new TimeValue(parse(sValue, normalized, "s"), TimeUnit.SECONDS);
370+
return new TimeValue(parse(sValue, normalized, "s", settingName), TimeUnit.SECONDS);
368371
} else if (sValue.endsWith("m")) {
369372
// parsing minutes should be case-sensitive as 'M' means "months", not "minutes"; this is the only special case.
370-
return new TimeValue(parse(sValue, normalized, "m"), TimeUnit.MINUTES);
373+
return new TimeValue(parse(sValue, normalized, "m", settingName), TimeUnit.MINUTES);
371374
} else if (normalized.endsWith("h")) {
372-
return new TimeValue(parse(sValue, normalized, "h"), TimeUnit.HOURS);
375+
return new TimeValue(parse(sValue, normalized, "h", settingName), TimeUnit.HOURS);
373376
} else if (normalized.endsWith("d")) {
374-
return new TimeValue(parse(sValue, normalized, "d"), TimeUnit.DAYS);
377+
return new TimeValue(parse(sValue, normalized, "d", settingName), TimeUnit.DAYS);
375378
} else if (normalized.matches("-0*1")) {
376379
return TimeValue.MINUS_ONE;
377380
} else if (normalized.matches("0+")) {
@@ -383,10 +386,16 @@ public static TimeValue parseTimeValue(String sValue, TimeValue defaultValue, St
383386
}
384387
}
385388

386-
private static long parse(final String initialInput, final String normalized, final String suffix) {
389+
private static long parse(final String initialInput, final String normalized, final String suffix, String settingName) {
387390
final String s = normalized.substring(0, normalized.length() - suffix.length()).trim();
388391
try {
389-
return Long.parseLong(s);
392+
final long value = Long.parseLong(s);
393+
if (value < -1) {
394+
// -1 is magic, but reject any other negative values
395+
throw new IllegalArgumentException("failed to parse setting [" + settingName + "] with value [" + initialInput +
396+
"] as a time value: negative durations are not supported");
397+
}
398+
return value;
390399
} catch (final NumberFormatException e) {
391400
try {
392401
@SuppressWarnings("unused") final double ignored = Double.parseDouble(s);

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

+22
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,26 @@ public void testConversionHashCode() {
238238
TimeValue secondValue = new TimeValue(firstValue.getSeconds(), TimeUnit.SECONDS);
239239
assertEquals(firstValue.hashCode(), secondValue.hashCode());
240240
}
241+
242+
public void testRejectsNegativeValuesDuringParsing() {
243+
final String settingName = "test-value";
244+
final long negativeValue = randomLongBetween(Long.MIN_VALUE, -2);
245+
final String negativeTimeValueString = Long.toString(negativeValue) + randomTimeUnit();
246+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
247+
() -> TimeValue.parseTimeValue(negativeTimeValueString, settingName));
248+
assertThat(ex.getMessage(),
249+
equalTo("failed to parse setting [" + settingName + "] with value [" + negativeTimeValueString +
250+
"] as a time value: negative durations are not supported"));
251+
}
252+
253+
public void testRejectsNegativeValuesAtCreation() {
254+
final long duration = randomLongBetween(Long.MIN_VALUE, -2);
255+
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new TimeValue(duration, randomTimeUnitObject()));
256+
assertThat(ex.getMessage(), containsString("duration cannot be negative"));
257+
}
258+
259+
private TimeUnit randomTimeUnitObject() {
260+
return randomFrom(TimeUnit.NANOSECONDS, TimeUnit.MICROSECONDS, TimeUnit.MILLISECONDS, TimeUnit.SECONDS,
261+
TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS);
262+
}
241263
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ public long getDeleteCount() {
133133
/**
134134
* The total amount of time spend on executing delete operations.
135135
*/
136-
public TimeValue getDeleteTime() { return new TimeValue(deleteTimeInMillis); }
136+
public TimeValue getDeleteTime() {
137+
return new TimeValue(deleteTimeInMillis);
138+
}
137139

138140
/**
139141
* Returns the currently in-flight delete operations

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

+12-5
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ public XContentBuilder toXContent(final XContentBuilder builder, final Params pa
530530
if (verbose || endTime != 0) {
531531
builder.field(END_TIME, DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(endTime).atZone(ZoneOffset.UTC)));
532532
builder.field(END_TIME_IN_MILLIS, endTime);
533-
builder.humanReadableField(DURATION_IN_MILLIS, DURATION, new TimeValue(endTime - startTime));
533+
builder.humanReadableField(DURATION_IN_MILLIS, DURATION, new TimeValue(Math.max(0L, endTime - startTime)));
534534
}
535535
if (verbose || !shardFailures.isEmpty()) {
536536
builder.startArray(FAILURES);

server/src/test/java/org/elasticsearch/action/search/SearchResponseMergerTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ private void awaitResponsesAdded() throws InterruptedException {
9494
}
9595

9696
public void testMergeTookInMillis() throws InterruptedException {
97-
long currentRelativeTime = randomLong();
97+
long currentRelativeTime = randomNonNegativeLong();
9898
SearchTimeProvider timeProvider = new SearchTimeProvider(randomLong(), 0, () -> currentRelativeTime);
9999
SearchResponseMerger merger = new SearchResponseMerger(randomIntBetween(0, 1000), randomIntBetween(0, 10000),
100100
SearchContext.TRACK_TOTAL_HITS_ACCURATE, timeProvider, emptyReduceContextBuilder());
101101
for (int i = 0; i < numResponses; i++) {
102-
SearchResponse searchResponse = new SearchResponse(InternalSearchResponse.empty(), null, 1, 1, 0, randomLong(),
102+
SearchResponse searchResponse = new SearchResponse(InternalSearchResponse.empty(), null, 1, 1, 0, randomNonNegativeLong(),
103103
ShardSearchFailure.EMPTY_ARRAY, SearchResponseTests.randomClusters());
104104
addResponse(merger, searchResponse);
105105
}
@@ -399,7 +399,7 @@ public void testMergeAggs() throws InterruptedException {
399399
}
400400

401401
public void testMergeSearchHits() throws InterruptedException {
402-
final long currentRelativeTime = randomLong();
402+
final long currentRelativeTime = randomNonNegativeLong();
403403
final SearchTimeProvider timeProvider = new SearchTimeProvider(randomLong(), 0, () -> currentRelativeTime);
404404
final int size = randomIntBetween(0, 100);
405405
final int from = size > 0 ? randomIntBetween(0, 100) : 0;
@@ -557,7 +557,7 @@ public void testMergeSearchHits() throws InterruptedException {
557557
}
558558

559559
public void testMergeNoResponsesAdded() {
560-
long currentRelativeTime = randomLong();
560+
long currentRelativeTime = randomNonNegativeLong();
561561
final SearchTimeProvider timeProvider = new SearchTimeProvider(randomLong(), 0, () -> currentRelativeTime);
562562
SearchResponseMerger merger = new SearchResponseMerger(0, 10, Integer.MAX_VALUE, timeProvider, emptyReduceContextBuilder());
563563
SearchResponse.Clusters clusters = SearchResponseTests.randomClusters();

server/src/test/java/org/elasticsearch/common/RoundingTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ public void testDayRounding() {
129129
Rounding tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH)
130130
.timeZone(ZoneOffset.ofHours(timezoneOffset)).build();
131131
assertThat(tzRounding.round(0), equalTo(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()));
132-
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(0L - TimeValue
133-
.timeValueHours(timezoneOffset).millis()));
132+
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(TimeValue
133+
.timeValueHours(-timezoneOffset).millis()));
134134

135135
ZoneId tz = ZoneId.of("-08:00");
136136
tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).timeZone(tz).build();

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ public void testDayRounding() {
128128
Rounding tzRounding = Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.forOffsetHours(timezoneOffset))
129129
.build();
130130
assertThat(tzRounding.round(0), equalTo(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()));
131-
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(0L - TimeValue
132-
.timeValueHours(timezoneOffset).millis()));
131+
assertThat(tzRounding.nextRoundingValue(0L - TimeValue.timeValueHours(24 + timezoneOffset).millis()), equalTo(TimeValue
132+
.timeValueHours(-timezoneOffset).millis()));
133133

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

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,17 @@ public void testNegativeInterval() {
253253
Exception e = expectThrows(IllegalArgumentException.class,
254254
() -> new MockController(Settings.builder()
255255
.put("indices.memory.interval", "-42s").build()));
256-
assertEquals("failed to parse value [-42s] for setting [indices.memory.interval], must be >= [0ms]", e.getMessage());
256+
assertEquals("failed to parse setting [indices.memory.interval] with value " +
257+
"[-42s] as a time value: negative durations are not supported", e.getMessage());
257258

258259
}
259260

260261
public void testNegativeShardInactiveTime() {
261262
Exception e = expectThrows(IllegalArgumentException.class,
262263
() -> new MockController(Settings.builder()
263264
.put("indices.memory.shard_inactive_time", "-42s").build()));
264-
assertEquals("failed to parse value [-42s] for setting [indices.memory.shard_inactive_time], must be >= [0ms]", e.getMessage());
265+
assertEquals("failed to parse setting [indices.memory.shard_inactive_time] with value " +
266+
"[-42s] as a time value: negative durations are not supported", e.getMessage());
265267

266268
}
267269

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

+5-4
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ protected ShardFollowNodeTaskStatus createTestInstance() {
6161
randomNonNegativeLong(),
6262
randomNonNegativeLong(),
6363
randomReadExceptions(),
64-
randomLong(),
64+
randomNonNegativeLong(),
6565
randomBoolean() ? new ElasticsearchException("fatal error") : null);
6666
}
6767

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ static FollowStatsAction.StatsResponses createStatsResponse() {
5959
randomNonNegativeLong(),
6060
randomNonNegativeLong(),
6161
Collections.emptyNavigableMap(),
62-
randomLong(),
62+
randomNonNegativeLong(),
6363
randomBoolean() ? new ElasticsearchException("fatal error") : null);
6464
responses.add(new FollowStatsAction.StatsResponse(status));
6565
}

0 commit comments

Comments
 (0)