Skip to content

Commit a391853

Browse files
committed
8475: Writing events with fields not explicitly set can corrupt the recording
1 parent 2c9ad54 commit a391853

File tree

5 files changed

+416
-14
lines changed

5 files changed

+416
-14
lines changed

core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/Chunk.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,6 @@ private void writeBuiltinType(LEB128Writer writer, TypedValueImpl typedValue) {
9393
throw new IllegalArgumentException();
9494
}
9595

96-
if (value == null && builtin != TypesImpl.Builtin.STRING) {
97-
// skip the non-string built-in values
98-
return;
99-
}
10096
switch (builtin) {
10197
case STRING: {
10298
if (value == null) {
@@ -114,35 +110,35 @@ private void writeBuiltinType(LEB128Writer writer, TypedValueImpl typedValue) {
114110
break;
115111
}
116112
case BYTE: {
117-
writer.writeByte((byte) value);
113+
writer.writeByte(value == null ? (byte) 0 : (byte) value);
118114
break;
119115
}
120116
case CHAR: {
121-
writer.writeChar((char) value);
117+
writer.writeChar(value == null ? (char) 0 : (char) value);
122118
break;
123119
}
124120
case SHORT: {
125-
writer.writeShort((short) value);
121+
writer.writeShort(value == null ? (short) 0 : (short) value);
126122
break;
127123
}
128124
case INT: {
129-
writer.writeInt((int) value);
125+
writer.writeInt(value == null ? 0 : (int) value);
130126
break;
131127
}
132128
case LONG: {
133-
writer.writeLong((long) value);
129+
writer.writeLong(value == null ? 0L : (long) value);
134130
break;
135131
}
136132
case FLOAT: {
137-
writer.writeFloat((float) value);
133+
writer.writeFloat(value == null ? 0.0f : (float) value);
138134
break;
139135
}
140136
case DOUBLE: {
141-
writer.writeDouble((double) value);
137+
writer.writeDouble(value == null ? 0.0 : (double) value);
142138
break;
143139
}
144140
case BOOLEAN: {
145-
writer.writeBoolean((boolean) value);
141+
writer.writeBoolean(value != null && (boolean) value);
146142
break;
147143
}
148144
default: {

core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/TypedValueImpl.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*/
3434
package org.openjdk.jmc.flightrecorder.writer;
3535

36+
import org.openjdk.jmc.flightrecorder.writer.api.Annotation;
3637
import org.openjdk.jmc.flightrecorder.writer.api.TypedValueBuilder;
3738
import org.openjdk.jmc.flightrecorder.writer.api.TypedValue;
3839
import org.openjdk.jmc.flightrecorder.writer.util.NonZeroHashCode;
@@ -142,13 +143,68 @@ public List<TypedFieldValueImpl> getFieldValues() {
142143
for (TypedFieldImpl field : type.getFields()) {
143144
TypedFieldValueImpl value = fields.get(field.getName());
144145
if (value == null) {
145-
value = new TypedFieldValueImpl(field, field.getType().nullValue());
146+
value = new TypedFieldValueImpl(field, getDefaultImplicitFieldValue(field));
146147
}
147148
values.add(value);
148149
}
149150
return values;
150151
}
151152

153+
/**
154+
* Gets the default value for a field when not explicitly provided by the user.
155+
* <p>
156+
* For event types (jdk.jfr.Event):
157+
* <ul>
158+
* <li>Fields annotated with {@code @Timestamp} receive {@link System#nanoTime()} as default,
159+
* providing a monotonic timestamp that will be >= the chunk's startTicks</li>
160+
* <li>Other fields receive null values</li>
161+
* </ul>
162+
* <p>
163+
* Note: JFR timestamps are stored as ticks relative to the chunk start, so the parser will
164+
* convert this absolute tick value to chunk-relative during reading.
165+
* <p>
166+
* <strong>Tick Frequency Assumption:</strong> This implementation assumes a 1:1 tick frequency
167+
* (1 tick = 1 nanosecond) as currently hardcoded in {@code RecordingImpl}. If the tick
168+
* frequency becomes configurable in the future, {@link System#nanoTime()} values will need to
169+
* be converted to ticks using: {@code nanoTime * ticksPerSecond / 1_000_000_000L}.
170+
*
171+
* @param field
172+
* the field to get default value for
173+
* @return the default value for the field
174+
*/
175+
private TypedValueImpl getDefaultImplicitFieldValue(TypedFieldImpl field) {
176+
if (!"jdk.jfr.Event".equals(type.getSupertype())) {
177+
return field.getType().nullValue();
178+
}
179+
180+
// Check if field is annotated with @Timestamp (any value means it's chunk-relative)
181+
if (hasTimestampAnnotation(field)) {
182+
// Use current nanoTime as default - will be valid and >= chunk startTicks
183+
// NOTE: Assumes 1:1 tick frequency (1 tick = 1 ns) as per RecordingImpl line 280
184+
return field.getType().asValue(System.nanoTime());
185+
}
186+
187+
// For all other fields, return null value
188+
// Null builtin values are handled properly by Chunk.writeBuiltinType()
189+
return field.getType().nullValue();
190+
}
191+
192+
/**
193+
* Checks if a field has the {@code @Timestamp} annotation.
194+
*
195+
* @param field
196+
* the field to check
197+
* @return true if the field is annotated with @Timestamp
198+
*/
199+
private boolean hasTimestampAnnotation(TypedFieldImpl field) {
200+
for (Annotation annotation : field.getAnnotations()) {
201+
if ("jdk.jfr.Timestamp".equals(annotation.getType().getTypeName())) {
202+
return true;
203+
}
204+
}
205+
return false;
206+
}
207+
152208
long getConstantPoolIndex() {
153209
return cpIndex;
154210
}

core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/Type.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,34 @@ public interface Type extends NamedType {
201201
TypedValue asValue(Object value);
202202

203203
/**
204+
* Creates a typed null value for this type.
205+
* <p>
206+
* Use this method when you need to pass a null value for optional or missing complex-type
207+
* fields. Passing {@code null} directly to
208+
* {@link TypedValueBuilder#putField(String, TypedValue)} causes compilation ambiguity because
209+
* the method is overloaded with multiple parameter types.
210+
* <p>
211+
* For primitive types (int, long, String, etc.), you can pass primitive default/null values
212+
* directly. For complex types (Thread, StackTrace, custom types), use this method to create a
213+
* properly typed null value.
214+
* <p>
215+
* <strong>Example:</strong>
216+
*
217+
* <pre>
218+
* {
219+
* &#64;code
220+
* Types types = recording.getTypes();
221+
* Type stackTraceType = types.getType(Types.JDK.STACK_TRACE);
222+
* Type threadType = types.getType(Types.JDK.THREAD);
223+
*
224+
* Type eventType = recording.registerEventType("custom.Event");
225+
* recording.writeEvent(eventType.asValue(builder -> {
226+
* builder.putField("startTime", System.nanoTime()).putField("stackTrace", stackTraceType.nullValue()) // typed null
227+
* .putField("eventThread", threadType.nullValue()); // typed null
228+
* }));
229+
* }
230+
* </pre>
231+
*
204232
* @return a specific {@linkplain TypedValue} instance designated as the {@literal null} value
205233
* for this type
206234
*/

core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/TypedValueBuilder.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,40 @@
3636
import java.util.Map;
3737
import java.util.function.Consumer;
3838

39-
/** A fluent API for lazy initialization of a composite type value */
39+
/**
40+
* A fluent API for lazy initialization of a composite type value.
41+
* <p>
42+
* This builder provides a chainable interface for setting field values in complex types. Use it
43+
* with {@link Type#asValue(java.util.function.Consumer)} to construct typed values.
44+
* <h2>Handling Null Values</h2>
45+
* <p>
46+
* When setting field values, avoid passing {@code null} directly as it causes compilation ambiguity
47+
* due to overloaded methods. Instead:
48+
* <ul>
49+
* <li>For primitive types (String, int, long, etc.): cast to the specific type, e.g.,
50+
* {@code (String) null}</li>
51+
* <li>For complex types (Thread, StackTrace, custom types): use {@link Type#nullValue()}</li>
52+
* </ul>
53+
* <p>
54+
* <strong>Example:</strong>
55+
*
56+
* <pre>
57+
* {
58+
* &#64;code
59+
* Types types = recording.getTypes();
60+
* Type threadType = types.getType(Types.JDK.THREAD);
61+
*
62+
* Type eventType = recording.registerEventType("custom.Event", builder -> {
63+
* builder.addField("message", Types.Builtin.STRING).addField("thread", Types.JDK.THREAD);
64+
* });
65+
*
66+
* recording.writeEvent(eventType.asValue(builder -> {
67+
* builder.putField("message", (String) null) // primitive null with cast
68+
* .putField("thread", threadType.nullValue()); // complex type null
69+
* }));
70+
* }
71+
* </pre>
72+
*/
4073
public interface TypedValueBuilder {
4174
Type getType();
4275

0 commit comments

Comments
 (0)