Skip to content

[BUG] Standalone mode is unusable on Windows: fileKey() is always null and the owner-only ACL omits the rights needed to repair itself #4400

Open

Description

@StreamPlume

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

Standalone mode (org.apache.hertzbeat.startup.HertzBeatApplication) cannot run on Windows 11. Two defects exist in the setup-file security layer, and both of them are hit during a normal start.

Symptom A — fatal failure before Spring starts. The process dies immediately with a message whose cause is discarded:

Exception in thread "main" org.apache.hertzbeat.startup.runtime.StandaloneDeploymentOwnerException: Standalone deployment ownership is unavailable
	at org.apache.hertzbeat.startup.runtime.StandaloneDeploymentOwnerException.unavailable(StandaloneDeploymentOwnerException.java:18)
	at org.apache.hertzbeat.startup.runtime.StandaloneDeploymentOwner.acquire(StandaloneDeploymentOwner.java:78)
	at org.apache.hertzbeat.startup.runtime.HertzBeatStartupCoordinator.acquireDeploymentOwner(HertzBeatStartupCoordinator.java:327)
	at org.apache.hertzbeat.startup.runtime.HertzBeatStartupCoordinator.start(HertzBeatStartupCoordinator.java:105)
	at org.apache.hertzbeat.startup.HertzBeatApplication.main(HertzBeatApplication.java:74)

Symptom B — silent degradation into an unrecoverable recovery_required state. After making the identity check non-fatal, the process starts but is permanently stuck in RECOVERY mode. GET /api/setup/status returns:

<StatusResponse>
  <phase>recovery_required</phase>
  <access>local</access>
  <applyMode>managed_write</applyMode>
  <writableManagedConfig>true</writableManagedConfig>
  <errorCode>config_recovery_required</errorCode>
  <managementDatabase><kind>h2</kind><configured>false</configured><source>built_in_default</source></managementDatabase>
</StatusResponse>

There is nothing to recover: data/config/ contains only lock files at this point.

.factory-reset.lock
.metadata-migration-operations.lock
.setup-transition-intent.lock
.standalone-deployment-owner.lock

The web UI shows only a read-only, button-less "The configuration needs to be recovered. The current server state has been preserved; follow the server recovery instructions." panel, and every product route is redirected back to /setup. No configuration property can escape this state — see the reproduction step 5.

Expected Behavior

  • Standalone mode should start on Windows. The surrounding pre-flight checks clearly intend Windows to be supported: StandaloneFileStorePolicy explicitly allows ntfs, and root resolution, secure file creation and owner-only ACL enforcement all succeed before the failure.
  • If Windows is genuinely out of scope, startup should fail fast with a clear, actionable message instead of an exception whose cause is discarded.
  • A failed lock/identity check must not be silently reported as "a configuration recovery is required". The current behaviour sends users hunting for a recovery procedure that does not exist, and no configuration property can bypass it.

Steps To Reproduce

  1. On Windows 11 (NTFS), build: mvnw.cmd -DskipTests clean install
  2. Run org.apache.hertzbeat.startup.HertzBeatApplication (IDE run configuration or the packaged distribution) with the project root as the working directory
  3. Observe the immediate StandaloneDeploymentOwnerException from Symptom A. Note that data/config/.standalone-deployment-owner.lock is created successfully before the failure, so steps 1–2 of acquire() pass and only the file-identity check fails
  4. (Optional, to reach Symptom B) Make the identity check non-fatal, then start again: the process now starts but GET /api/setup/status reports phase=recovery_required, and the UI redirects every route to a read-only /setup page
  5. (Optional, to confirm there is no escape hatch) Start with --hertzbeat.startup.mode=normal (or -Dhertzbeat.startup.mode=normal, or HERTZBEAT_STARTUP_MODE=normal). The property has no effect, because the migration preflight branch of HertzBeatStartupCoordinator.startupPlan() is taken and never calls the installation probe

Environment

HertzBeat version(s): 2.0-SNAPSHOT (branch 2.0.0, commit 40e7485be "Stabilize migration recovery timing test")
OS: Windows 11 (NTFS)
JDK: Eclipse Temurin 26.0.2.1 (used for the reproduction; the fileKey() limitation is a Windows platform behaviour, not JDK-version-specific)
Entry point: org.apache.hertzbeat.startup.HertzBeatApplication (standalone)
Working directory: project root (installation root); data/config/ is created there
Database: never reached — the failure happens before the Spring context starts

Debug logs

1. BasicFileAttributes.fileKey() is always null on Windows. Minimal probe on the affected machine:

JVM       = 26.0.2.1
OS        = Windows 11
store     = NTFS
rootKey   = null
lockKey   = null

2. The exception cause is discarded — StandaloneDeploymentOwner.acquire(...) replaces the real IOException with StandaloneDeploymentOwnerException.unavailable(). There is no initCause and no log line, so the actual reason ("identity is unavailable") is not visible to the user or to a log collector. The same static factory is also used for the genuinely different "another process holds the lock" case (FileLock == null), so the message cannot distinguish a lost race from a platform limitation.

3. data/config/ after the failed start (lock file created, then startup aborted):

.factory-reset.lock
.metadata-migration-operations.lock
.setup-transition-intent.lock
.standalone-deployment-owner.lock

4. The migration-preflight failure is swallowed with no diagnostics. Once the identity check is non-fatal, the lock handshake in SecureSetupFileLock fails instead, and the exception disappears entirely:

  • SecureSetupFileLock.execute(...) → initializeAndValidate() throws IOException
  • FileMigrationOperationStore.locked(...) converts any IOException into MigrationOperationStoreException(CONFIG_RECOVERY_REQUIRED)
  • ManagedMigrationStartupRecoverySession.selectOnce() catches it and returns GATED_RECOVERY without logging
  • HertzBeatStartupCoordinator.startupPlan() maps GATED_RECOVERY to StartupDecision.recovery()
  • SetupRuntimeStateFactory.phase() returns RECOVERY_REQUIRED for mode == RECOVERY

StartupFailureReporter is not invoked on this path, and even when it is, it logs only the exception class name, never the message or cause.

5. Existing test evidence that the startup property is bypassed. HertzBeatStartupMigrationPreflightFlowTest passes --hertzbeat.startup.mode=normal while the preflight returns GATED_RECOVERY and asserts that the probe is never called:

coordinator.start(new String[] {
        "--" + SetupInstallationPaths.ROOT_PROPERTY + "=" + installationRoot,
        "--" + StartupModePropertyProbe.PROPERTY_NAME + "=" + RuntimeMode.NORMAL.value()
});
assertEquals(RuntimeMode.RECOVERY, coordinator.mode());
...
assertEquals(0, probes.get());

6. Only clean workaround found: delete the data/ directory (four lock files plus a half-initialised H2 file) and restart.

Anything else?

Affected files

File Defect
hertzbeat-startup/.../runtime/StandaloneDeploymentOwner.java Strict fileKey() identity check always fails on Windows; the resulting exception hides its cause; the immediate reopen of the just-created lock file has no retry against Windows AV/indexer deny-share handles; a stale lock file is never repaired
hertzbeat-manager/.../setup/security/SecureSetupFileLock.java Same fileKey() defect (thrown instead of tolerated); the same reopen-without-retry pattern; no DACL re-enforcement for an existing lock file; failures are swallowed upstream into recovery_required
hertzbeat-manager/.../setup/security/OwnerOnlyFilePermissions.java The written ACL_PERMISSIONS set omits WRITE_ACL and the extended-attribute rights, so the ACL cannot be re-applied to the very file it protects (self-inflicted lockout)

Root cause 1 — fileKey() is unusable on Windows

// StandaloneDeploymentOwner
private static Object fileKey(Path path) throws IOException {
    Object key = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS).fileKey();
    if (key == null) {
        throw new IOException("Standalone deployment owner identity is unavailable");
    }
    return key;
}

// SecureSetupFileLock
private Object readPathFileKey() throws IOException {
    BasicFileAttributes attributes = Files.readAttributes(
            lockFile, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    if (attributes.fileKey() == null) {
        throw new IOException("Secure setup-file lock identity is unavailable");
    }
    return attributes.fileKey();
}

BasicFileAttributes.fileKey() returns null on Windows — a long-standing JDK platform behaviour (no file identity key is provided by the Windows provider). Every caller that relies on it therefore fails closed on this platform.

Root cause 2 — the ACL written at creation forbids repairing that same ACL

Activity

  1. xdn-code commented on Oct 4, 2026

    @xdn-code
    Contributor

    Hi @StreamPlume, thank you for the detailed report, and for tracing the failure to fileKey() and the owner-only ACL.

    I would like to take this on, if that is all right. I will work from the current 2.0.0 branch and:

    • tolerate a null fileKey() in StandaloneDeploymentOwner and SecureSetupFileLock
    • keep the original IOException cause instead of discarding it
    • include WRITE_ACL so OwnerOnlyFilePermissions can re-apply the ACL

    I will add unit tests and verify standalone startup on Windows before opening the PR. @Duansg, please let me know if this overlaps with anything already in progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2.0.0bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions