From 48e5246067c4e7302238387e7e71db5c4b4f4568 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 10 Aug 2026 14:25:01 +0200 Subject: [PATCH] fix(ZBytes): publish the lazy byte cache safely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lazy materialization in ZBytes is double-checked locking: the fast path reads `eager` outside the monitor while the slow path writes it under it. Without volatile, a reader observing the non-null reference has no happens-before edge to the copy that filled the array. Mark `eager` volatile. It moves out of the primary constructor because @Volatile targets fields. The synchronized slow path stays: it is what guarantees exactly one thread copies out of and closes the native handle. `handle` stays non-volatile — it is only touched inside the monitor. Closes #516 --- .../src/commonMain/kotlin/io/zenoh/bytes/ZBytes.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/ZBytes.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/ZBytes.kt index 5822fd00..9d2927d4 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/ZBytes.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/ZBytes.kt @@ -48,10 +48,19 @@ import io.zenoh.jni.bytes.ZBytes as JniZBytes * dropped samples are the one place native memory can be retained. */ class ZBytes private constructor( - private var eager: ByteArray?, + initialBytes: ByteArray?, private var handle: JniZBytes?, ) : IntoZBytes { + /** + * The materialized bytes, `null` until a handle-backed ZBytes is read. + * Volatile: the [bytes] getter reads it outside the monitor, so the write + * under the monitor must be safely published to that unlocked fast path. + * `handle` needs no such treatment — it is only touched inside the monitor. + */ + @Volatile + private var eager: ByteArray? = initialBytes + internal constructor(bytes: ByteArray) : this(bytes, null) /**