commit 659b192545075967dae606c899e2e7bf7d66d06f
parent e5761180533e6479015c3c91d70bc525a9f3aab2
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 04:42:49 +0000
package: isolate packaged health-check data
- require a fresh absolute health data root before native open
- guard cleanup with a versioned create-new ownership marker
- preserve caller-owned roots and retain data when shutdown fails
- test invalid roots, redaction, cleanup, and packaged execution
Diffstat:
4 files changed, 340 insertions(+), 24 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/desktop/Main.kt b/app/desktop/src/main/kotlin/org/harvestcircle/desktop/Main.kt
@@ -20,6 +20,14 @@ import org.harvestcircle.ffi.compatibilityDescriptor
import org.harvestcircle.identities.ui.StartupFailureScreen
import java.awt.Dimension
import java.awt.Taskbar
+import java.nio.file.FileVisitResult
+import java.nio.file.Files
+import java.nio.file.LinkOption
+import java.nio.file.Path
+import java.nio.file.SimpleFileVisitor
+import java.nio.file.StandardOpenOption
+import java.nio.file.attribute.BasicFileAttributes
+import java.security.SecureRandom
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@@ -34,8 +42,12 @@ internal const val MINIMUM_WINDOW_HEIGHT = 720
internal const val HEALTH_CHECK_ARGUMENT = "--health-check"
internal const val HEALTH_READY_EVIDENCE = "HARVESTCIRCLE_HEALTH_READY"
internal const val HEALTH_CLOSED_EVIDENCE = "HARVESTCIRCLE_HEALTH_CLOSED"
+internal const val HEALTH_DATA_ENVIRONMENT = "HARVESTCIRCLE_DEVELOPMENT_DATA_DIR"
private const val HEALTH_FAILURE_EVIDENCE = "HARVESTCIRCLE_HEALTH_FAILED"
private const val HEALTH_TIMEOUT_MILLIS = 90_000L
+private const val HEALTH_OWNER_MARKER = ".harvestcircle-health-owner-v1"
+private const val HEALTH_OWNER_TOKEN_BYTES = 16
+private val healthOwnershipRandom = SecureRandom()
private val isMacOs: Boolean =
System
@@ -90,9 +102,15 @@ internal fun isHealthCheck(args: Array<String>): Boolean = args.size == 1 && arg
private fun runHealthCheck(): Int {
val executor = Executors.newSingleThreadExecutor()
return try {
- executor
- .submit<Int> { runBlocking { executeHealthCheck() } }
- .get(HEALTH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ val healthCheck =
+ executor.submit<Int> {
+ runBlocking {
+ withTimeout(HEALTH_TIMEOUT_MILLIS) {
+ executeHealthCheck(System.getenv(HEALTH_DATA_ENVIRONMENT))
+ }
+ }
+ }
+ healthCheck.get(HEALTH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
} catch (_: TimeoutException) {
System.err.println("$HEALTH_FAILURE_EVIDENCE:TIMEOUT")
1
@@ -101,40 +119,161 @@ private fun runHealthCheck(): Int {
}
}
-private suspend fun executeHealthCheck(): Int {
- var runtime: NativeHarvestCircleRuntime? = null
+internal interface PackagedHealthRuntime {
+ suspend fun bootstrapLifecycle(): ApplicationLifecycle
+
+ suspend fun shutdownClosed(): Boolean
+}
+
+internal fun interface PackagedHealthRuntimeOpener {
+ fun open(dataRoot: Path): PackagedHealthRuntime
+}
+
+private class NativePackagedHealthRuntime(
+ private val runtime: NativeHarvestCircleRuntime,
+) : PackagedHealthRuntime {
+ override suspend fun bootstrapLifecycle(): ApplicationLifecycle = runtime.bootstrap().lifecycle
+
+ override suspend fun shutdownClosed(): Boolean = runtime.shutdown().closed
+}
+
+internal suspend fun executeHealthCheck(
+ developmentDataDirectory: String?,
+ timeoutMillis: Long = HEALTH_TIMEOUT_MILLIS,
+ runtimeOpener: PackagedHealthRuntimeOpener =
+ PackagedHealthRuntimeOpener {
+ NativePackagedHealthRuntime(NativeHarvestCircleRuntime.open(developmentMode = true))
+ },
+ standardOutput: (String) -> Unit = ::println,
+ errorOutput: (String) -> Unit = System.err::println,
+): Int {
+ var ownedRoot: OwnedHealthDataRoot? = null
+ var runtime: PackagedHealthRuntime? = null
var closed = false
- var stage = "OPEN"
+ var stage = "ROOT"
+ var failureEvidence: String? = null
try {
- withTimeout(HEALTH_TIMEOUT_MILLIS) {
+ withTimeout(timeoutMillis) {
+ ownedRoot = claimHealthDataRoot(developmentDataDirectory)
stage = "COMPATIBILITY"
verifyNativeCompatibility(compatibilityDescriptor())
stage = "OPEN"
- runtime = NativeHarvestCircleRuntime.open(developmentMode = true)
+ runtime = runtimeOpener.open(requireNotNull(ownedRoot).path)
stage = "BOOTSTRAP"
- val snapshot = requireNotNull(runtime).bootstrap()
+ val lifecycle = requireNotNull(runtime).bootstrapLifecycle()
stage = "READY"
- require(snapshot.lifecycle in setOf(ApplicationLifecycle.Ready, ApplicationLifecycle.Degraded))
- println(HEALTH_READY_EVIDENCE)
+ require(lifecycle in setOf(ApplicationLifecycle.Ready, ApplicationLifecycle.Degraded))
+ standardOutput(HEALTH_READY_EVIDENCE)
stage = "SHUTDOWN"
- val receipt = requireNotNull(runtime).shutdown()
- require(receipt.closed)
+ require(requireNotNull(runtime).shutdownClosed())
closed = true
- println(HEALTH_CLOSED_EVIDENCE)
+ standardOutput(HEALTH_CLOSED_EVIDENCE)
}
- return 0
+ } catch (_: kotlinx.coroutines.TimeoutCancellationException) {
+ failureEvidence = "$HEALTH_FAILURE_EVIDENCE:TIMEOUT"
} catch (error: HarvestCircleException.Failure) {
- System.err.println("$HEALTH_FAILURE_EVIDENCE:$stage:${error.code}:${error.safeMessage}")
- return 1
+ failureEvidence = "$HEALTH_FAILURE_EVIDENCE:$stage:${error.code}:${error.safeMessage}"
} catch (error: Exception) {
- System.err.println("$HEALTH_FAILURE_EVIDENCE:$stage:${error.javaClass.simpleName}")
- return 1
+ failureEvidence = "$HEALTH_FAILURE_EVIDENCE:$stage:${error.javaClass.simpleName}"
} finally {
- if (!closed) {
- runCatching {
- withTimeout(HEALTH_TIMEOUT_MILLIS) { runtime?.shutdown() }
+ var safeToCleanup = runtime == null || closed
+ if (!closed && runtime != null) {
+ val shutdown = runCatching { withTimeout(timeoutMillis) { requireNotNull(runtime).shutdownClosed() } }
+ if (shutdown.getOrDefault(false)) {
+ closed = true
+ safeToCleanup = true
+ } else if (failureEvidence == null) {
+ failureEvidence = "$HEALTH_FAILURE_EVIDENCE:SHUTDOWN:IllegalStateException"
}
}
+ ownedRoot?.takeIf { safeToCleanup }?.let { root ->
+ runCatching { root.cleanup() }
+ .onFailure {
+ if (failureEvidence == null) {
+ failureEvidence = "$HEALTH_FAILURE_EVIDENCE:CLEANUP:${it.javaClass.simpleName}"
+ }
+ }
+ }
+ }
+ failureEvidence?.let(errorOutput)
+ return if (failureEvidence == null) 0 else 1
+}
+
+internal class OwnedHealthDataRoot internal constructor(
+ val path: Path,
+ private val marker: Path,
+ private val ownershipToken: String,
+ private val createdByHealthCheck: Boolean,
+) {
+ fun cleanup() {
+ require(Files.exists(path, LinkOption.NOFOLLOW_LINKS) && Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS))
+ requireNoSymbolicLinks(path)
+ require(path.toRealPath() == path)
+ require(Files.isRegularFile(marker, LinkOption.NOFOLLOW_LINKS) && !Files.isSymbolicLink(marker))
+ require(Files.readString(marker) == ownershipToken)
+
+ Files.walkFileTree(
+ path,
+ object : SimpleFileVisitor<Path>() {
+ override fun visitFile(
+ file: Path,
+ attributes: BasicFileAttributes,
+ ): FileVisitResult {
+ Files.delete(file)
+ return FileVisitResult.CONTINUE
+ }
+
+ override fun postVisitDirectory(
+ directory: Path,
+ error: java.io.IOException?,
+ ): FileVisitResult {
+ if (error != null) throw error
+ if (directory != path || createdByHealthCheck) Files.delete(directory)
+ return FileVisitResult.CONTINUE
+ }
+ },
+ )
+ }
+}
+
+internal fun claimHealthDataRoot(rawPath: String?): OwnedHealthDataRoot {
+ require(!rawPath.isNullOrBlank()) { "A dedicated health data root is required" }
+ val requested = Path.of(rawPath).normalize()
+ require(requested.isAbsolute) { "The health data root must be absolute" }
+ val parent = requireNotNull(requested.parent) { "The health data root must have a parent" }
+ requireNoSymbolicLinks(parent)
+ require(Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { "The health data parent must exist" }
+
+ val created = !Files.exists(requested, LinkOption.NOFOLLOW_LINKS)
+ if (created) {
+ Files.createDirectory(requested)
+ } else {
+ require(!Files.isSymbolicLink(requested)) { "The health data root cannot be a symbolic link" }
+ require(Files.isDirectory(requested, LinkOption.NOFOLLOW_LINKS)) { "The health data root must be a directory" }
+ Files.list(requested).use { entries -> require(entries.findAny().isEmpty) { "The health data root must be empty" } }
+ }
+
+ return try {
+ requireNoSymbolicLinks(requested)
+ val canonical = requested.toRealPath()
+ val token =
+ ByteArray(HEALTH_OWNER_TOKEN_BYTES)
+ .also(healthOwnershipRandom::nextBytes)
+ .joinToString(separator = "") { byte -> "%02x".format(byte.toInt() and 0xff) }
+ val marker = canonical.resolve(HEALTH_OWNER_MARKER)
+ Files.writeString(marker, token, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
+ OwnedHealthDataRoot(canonical, marker, token, created)
+ } catch (error: Exception) {
+ if (created) runCatching { Files.deleteIfExists(requested) }
+ throw error
+ }
+}
+
+private fun requireNoSymbolicLinks(path: Path) {
+ var current = requireNotNull(path.root) { "The health data root must be absolute" }
+ path.forEach { segment ->
+ current = current.resolve(segment)
+ require(!Files.isSymbolicLink(current)) { "The health data root cannot traverse a symbolic link" }
}
}
diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/desktop/MainTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/desktop/MainTest.kt
@@ -1,8 +1,13 @@
package org.harvestcircle.desktop
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.harvestcircle.application.ApplicationLifecycle
import java.io.ByteArrayInputStream
+import java.nio.file.Files
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -28,4 +33,159 @@ class MainTest {
assertFalse(isHealthCheck(emptyArray()))
assertFalse(isHealthCheck(arrayOf(HEALTH_CHECK_ARGUMENT, "unexpected")))
}
+
+ @Test
+ fun invalidHealthRootsFailBeforeTheRuntimeOpener() =
+ runBlocking {
+ val parent = Files.createTempDirectory("harvestcircle-health-invalid-").toRealPath()
+ val nonempty = Files.createDirectory(parent.resolve("nonempty"))
+ Files.writeString(nonempty.resolve("foreign"), "owned elsewhere")
+ val file = Files.writeString(parent.resolve("file"), "not a directory")
+ val target = Files.createDirectory(parent.resolve("target"))
+ var openCalls = 0
+ val opener =
+ PackagedHealthRuntimeOpener {
+ openCalls += 1
+ successfulRuntime()
+ }
+
+ val invalidRoots =
+ mutableListOf<String?>(
+ null,
+ "",
+ "relative/path",
+ parent.resolve("missing-parent/root").toString(),
+ nonempty.toString(),
+ file.toString(),
+ )
+ runCatching { Files.createSymbolicLink(parent.resolve("link"), target) }
+ .getOrNull()
+ ?.let { invalidRoots += it.toString() }
+
+ invalidRoots.forEach { invalid ->
+ assertEquals(1, executeHealthCheck(invalid, runtimeOpener = opener, errorOutput = {}))
+ }
+ assertEquals(0, openCalls)
+ }
+
+ @Test
+ fun healthCheckOwnsBootstrapsShutsDownAndCleansCreatedAndProvidedRoots() =
+ runBlocking {
+ val parent = Files.createTempDirectory("harvestcircle-health-valid-").toRealPath()
+ val createdRoot = parent.resolve("created")
+ val providedRoot = Files.createDirectory(parent.resolve("provided"))
+ val output = mutableListOf<String>()
+ var openCalls = 0
+
+ for (root in listOf(createdRoot, providedRoot)) {
+ val result =
+ executeHealthCheck(
+ root.toString(),
+ runtimeOpener =
+ PackagedHealthRuntimeOpener { ownedRoot ->
+ openCalls += 1
+ Files.writeString(ownedRoot.resolve("runtime.sqlite3"), "runtime data")
+ successfulRuntime()
+ },
+ standardOutput = output::add,
+ errorOutput = output::add,
+ )
+ assertEquals(0, result)
+ }
+
+ assertEquals(2, openCalls)
+ assertFalse(Files.exists(createdRoot))
+ assertTrue(Files.isDirectory(providedRoot))
+ Files.list(providedRoot).use { entries -> assertTrue(entries.findAny().isEmpty) }
+ assertEquals(
+ listOf(HEALTH_READY_EVIDENCE, HEALTH_CLOSED_EVIDENCE, HEALTH_READY_EVIDENCE, HEALTH_CLOSED_EVIDENCE),
+ output,
+ )
+ }
+
+ @Test
+ fun cleanupRefusesMissingOrChangedOwnershipMarkersWithoutDeletingData() {
+ val parent = Files.createTempDirectory("harvestcircle-health-marker-").toRealPath()
+ listOf("missing", "changed").forEach { caseName ->
+ val root = Files.createDirectory(parent.resolve(caseName))
+ val ownership = claimHealthDataRoot(root.toString())
+ val marker = Files.list(root).use { entries -> entries.findFirst().orElseThrow() }
+ val retained = Files.writeString(root.resolve("retained"), "caller data")
+ if (caseName == "missing") Files.delete(marker) else Files.writeString(marker, "different owner")
+
+ assertFailsWith<IllegalArgumentException> { ownership.cleanup() }
+ assertTrue(Files.exists(retained))
+ }
+ }
+
+ @Test
+ fun timeoutAndFailureEvidenceDoNotExposeExceptionSecrets() =
+ runBlocking {
+ val parent = Files.createTempDirectory("harvestcircle-health-redaction-").toRealPath()
+ val secret = "nsec1must-not-escape"
+ val evidence = mutableListOf<String>()
+ val timeoutRuntime =
+ object : PackagedHealthRuntime {
+ override suspend fun bootstrapLifecycle(): ApplicationLifecycle {
+ delay(50)
+ return ApplicationLifecycle.Ready
+ }
+
+ override suspend fun shutdownClosed(): Boolean = true
+ }
+
+ assertEquals(
+ 1,
+ executeHealthCheck(
+ parent.resolve("timeout").toString(),
+ timeoutMillis = 1,
+ runtimeOpener = PackagedHealthRuntimeOpener { timeoutRuntime },
+ errorOutput = evidence::add,
+ ),
+ )
+ assertEquals(
+ 1,
+ executeHealthCheck(
+ parent.resolve("failure").toString(),
+ runtimeOpener = PackagedHealthRuntimeOpener { throw IllegalStateException(secret) },
+ errorOutput = evidence::add,
+ ),
+ )
+ assertTrue(evidence.any { it == "HARVESTCIRCLE_HEALTH_FAILED:TIMEOUT" })
+ assertTrue(evidence.any { it.contains(":OPEN:IllegalStateException") })
+ assertFalse(evidence.joinToString().contains(secret))
+ assertFalse(Files.exists(parent.resolve("timeout")))
+ assertFalse(Files.exists(parent.resolve("failure")))
+ }
+
+ @Test
+ fun failedShutdownRetainsTheOwnedRootInsteadOfDeletingLiveRuntimeData() =
+ runBlocking {
+ val parent = Files.createTempDirectory("harvestcircle-health-shutdown-").toRealPath()
+ val root = parent.resolve("retained")
+ val runtime =
+ object : PackagedHealthRuntime {
+ override suspend fun bootstrapLifecycle(): ApplicationLifecycle = ApplicationLifecycle.Ready
+
+ override suspend fun shutdownClosed(): Boolean = false
+ }
+
+ assertEquals(
+ 1,
+ executeHealthCheck(
+ root.toString(),
+ runtimeOpener = PackagedHealthRuntimeOpener { runtime },
+ errorOutput = {},
+ ),
+ )
+ assertTrue(Files.isDirectory(root))
+ Files.list(root).use { entries -> assertTrue(entries.findAny().isPresent) }
+ }
}
+
+private fun successfulRuntime(): PackagedHealthRuntime =
+ object : PackagedHealthRuntime {
+ override suspend fun bootstrapLifecycle(): ApplicationLifecycle = ApplicationLifecycle.Ready
+
+ override suspend fun shutdownClosed(): Boolean = true
+ }
diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt
@@ -296,7 +296,13 @@ class ConventionPluginSmokeTest {
@Test
fun packagingPluginLaunchesAndClosesThePackagedHealthEntry() {
val fixture = createTempDirectory("harvestcircle-package-health-")
- preparePackagingBuild(fixture, "printf 'HARVESTCIRCLE_HEALTH_READY\\nHARVESTCIRCLE_HEALTH_CLOSED\\n'")
+ preparePackagingBuild(
+ fixture,
+ "test -n \"\${HARVESTCIRCLE_DEVELOPMENT_DATA_DIR:-}\" && " +
+ "test -d \"\$HARVESTCIRCLE_DEVELOPMENT_DATA_DIR\" && " +
+ "test -z \"\$(find \"\$HARVESTCIRCLE_DEVELOPMENT_DATA_DIR\" -mindepth 1 -print -quit)\" && " +
+ "printf 'HARVESTCIRCLE_HEALTH_READY\\nHARVESTCIRCLE_HEALTH_CLOSED\\n'",
+ )
val result =
GradleRunner.create()
@@ -323,6 +329,12 @@ class ConventionPluginSmokeTest {
"printf 'nsec1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa HARVESTCIRCLE_HEALTH_READY HARVESTCIRCLE_HEALTH_CLOSED\\n'",
"emitted secret material",
),
+ Triple(
+ "residue",
+ "touch \"\$HARVESTCIRCLE_DEVELOPMENT_DATA_DIR/leftover\"; " +
+ "printf 'HARVESTCIRCLE_HEALTH_READY\\nHARVESTCIRCLE_HEALTH_CLOSED\\n'",
+ "did not clean its isolated health data root",
+ ),
).forEach { (caseName, scriptBody, expected) ->
val fixture = createTempDirectory("harvestcircle-package-$caseName-")
preparePackagingBuild(fixture, scriptBody, timeoutSeconds = if (caseName == "timeout") 1L else 10L)
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/tasks/PackagingTasks.kt
@@ -16,6 +16,7 @@ import org.gradle.process.ExecOperations
import org.gradle.work.DisableCachingByDefault
import java.io.ByteArrayOutputStream
import java.io.File
+import java.nio.file.Files
import java.util.concurrent.TimeUnit
import java.util.jar.JarFile
import javax.inject.Inject
@@ -233,7 +234,7 @@ public abstract class VerifyPackagedApplicationHealth : DefaultTask() {
@TaskAction
public fun verify() {
- val dataRoot = temporaryDir.resolve("isolated-data").apply { mkdirs() }
+ val dataRoot = Files.createTempDirectory(temporaryDir.toPath(), "isolated-data-").toFile()
val process =
ProcessBuilder(executable.get().asFile.absolutePath, "--health-check")
.redirectErrorStream(true)
@@ -250,6 +251,10 @@ public abstract class VerifyPackagedApplicationHealth : DefaultTask() {
require(process.exitValue() == 0) { "Packaged application health-check failed" }
require(output.contains(readyEvidence.get())) { "Packaged application did not report ready health evidence" }
require(output.contains(closedEvidence.get())) { "Packaged application did not report closed health evidence" }
+ require(!dataRoot.exists() || dataRoot.list()?.isEmpty() == true) {
+ "Packaged application did not clean its isolated health data root"
+ }
+ if (dataRoot.exists()) require(dataRoot.delete()) { "Isolated health data root could not be removed" }
}
}