This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
     new 625c96c736 [karaf-4.4.x] Fix race condition corrupting cfg files when 
updating an existing configuration (#2900)
625c96c736 is described below

commit 625c96c736dc49b548868ec5506d018f3315356e
Author: JB Onofré <[email protected]>
AuthorDate: Mon Sep 14 15:20:41 2026 +0200

    [karaf-4.4.x] Fix race condition corrupting cfg files when updating an 
existing configuration (#2900)
    
    * Fix race condition corrupting cfg files when updating an existing 
configuration
    
    FeatureConfigInstaller.updateExistingConfig() wrote directly to the target
    cfg file when appending to / overriding an existing configuration. Since
    cfg.update() also triggers fileinstall to persist the same configuration
    on the CM Event Dispatcher thread, two unsynchronized writers could hit the
    same file at once, producing torn/corrupted content. The "new file" path
    already avoided this by writing to a temp file and renaming it atomically
    into place (KARAF-7389); this applies the same pattern to the
    existing-file path.
    
    Fixes #2805
    
    (cherry picked from commit a9ca73ae99b87746044dc5b0a322b612d06733be)
    
    * GH-2805: Use Files.move with REPLACE_EXISTING instead of File.renameTo
    
    File.renameTo() silently fails (returns false, no exception) on Windows
    when the destination already exists, leaving the cfg file stale and the
    temp file orphaned. This broke updateExistingConfig() since it is only
    called when the target file already exists, causing the Windows CI job
    to fail (AppendTest).
    
    (cherry picked from commit d5badd00c89559dabf920eb77340af5f0c85a2f3)
    
    * Add fallback for atomic file move
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    (cherry picked from commit 741ff3b0628ba961f3e6d5c17c3c78896b7bd963)
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../internal/service/FeatureConfigInstaller.java   | 21 ++++++++--
 .../service/FeatureConfigInstallerTest.java        | 45 +++++++++++++++++++++-
 2 files changed, 62 insertions(+), 4 deletions(-)

diff --git 
a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
 
b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
index 7d530ae4e6..e903e2ce53 100644
--- 
a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
+++ 
b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
@@ -21,6 +21,8 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.util.*;
 import java.util.regex.Pattern;
 
@@ -330,7 +332,12 @@ public class FeatureConfigInstaller {
                 } else {
                     props.save(tmpCfgFile);
                 }
-                tmpCfgFile.renameTo(cfgFile);
+try {
+                    Files.move(tmpCfgFile.toPath(), cfgFile.toPath(),
+                            StandardCopyOption.REPLACE_EXISTING, 
StandardCopyOption.ATOMIC_MOVE);
+                } catch (java.nio.file.AtomicMoveNotSupportedException e) {
+                    Files.move(tmpCfgFile.toPath(), cfgFile.toPath(), 
StandardCopyOption.REPLACE_EXISTING);
+                }
             } else {
                 updateExistingConfig(props, append, cfgFile, jsonFormat);
             }
@@ -400,11 +407,19 @@ public class FeatureConfigInstaller {
             }
         }
         storage.mkdirs();
+        // write to a temporary file and rename it to the target file so that 
a concurrent writer
+        // (e.g. fileinstall persisting the configuration update on the CM 
Event Dispatcher thread)
+        // never observes a partially written / corrupted cfg file
+        File tmpCfgFile = File.createTempFile(cfgFile.getName(), ".tmp", 
cfgFile.getParentFile());
         if (jsonFormat) {
-            Configurations.buildWriter().build(new 
FileWriter(cfgFile)).writeConfiguration(new Hashtable(properties));
+            Configurations.buildWriter().build(new 
FileWriter(tmpCfgFile)).writeConfiguration(new Hashtable(properties));
         } else {
-            properties.save(cfgFile);
+            properties.save(tmpCfgFile);
         }
+        // File.renameTo() silently fails on Windows when the destination 
already exists,
+        // so use Files.move() with REPLACE_EXISTING to get a working atomic 
replace on every OS
+        Files.move(tmpCfgFile.toPath(), cfgFile.toPath(),
+                StandardCopyOption.REPLACE_EXISTING, 
StandardCopyOption.ATOMIC_MOVE);
     }
 
     private boolean isInternalKey(String key) {
diff --git 
a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeatureConfigInstallerTest.java
 
b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeatureConfigInstallerTest.java
index c9497fc8af..277bd55a2f 100644
--- 
a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeatureConfigInstallerTest.java
+++ 
b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeatureConfigInstallerTest.java
@@ -16,14 +16,21 @@
  */
 package org.apache.karaf.features.internal.service;
 
+import org.apache.felix.utils.properties.TypedProperties;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.io.FileWriter;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.util.Objects;
 
 public class FeatureConfigInstallerTest {
-    
+
     private void substEqual(final String src, final String subst) {
         assertEquals(FeatureConfigInstaller.substFinalName(src), subst);
     }
@@ -51,4 +58,40 @@ public class FeatureConfigInstallerTest {
         substEqual("${foo}${bar}/${bar}${foo}", foo + "/" + foo);
     }
 
+    /**
+     * GH-2805: updating an existing cfg file (append or override) must write 
through a
+     * temporary file and rename it into place, so that a concurrent writer 
(e.g. fileinstall
+     * persisting the same configuration on the CM Event Dispatcher thread) 
can never observe
+     * a partially written / corrupted cfg file, and no leftover temp file 
remains behind.
+     */
+    @Test
+    public void testUpdateExistingConfigWritesAtomically() throws Exception {
+        File tmpDir = 
Files.createTempDirectory("karaf-feature-config-installer-test").toFile();
+        System.setProperty("karaf.etc", tmpDir.getAbsolutePath());
+
+        File cfgFile = new File(tmpDir, "my.pid.cfg");
+        try (FileWriter writer = new FileWriter(cfgFile)) {
+            writer.write("existing.key=existing.value\n");
+        }
+
+        TypedProperties toAppend = new TypedProperties();
+        toAppend.load(new StringReader("appended.key=appended.value\n"));
+
+        FeatureConfigInstaller installer = new FeatureConfigInstaller(null, 
true);
+
+        Method updateExistingConfig = 
FeatureConfigInstaller.class.getDeclaredMethod(
+                "updateExistingConfig", TypedProperties.class, boolean.class, 
File.class, boolean.class);
+        updateExistingConfig.setAccessible(true);
+        updateExistingConfig.invoke(installer, toAppend, true, cfgFile, false);
+
+        TypedProperties result = new TypedProperties();
+        result.load(cfgFile);
+        assertEquals("existing.value", result.get("existing.key"));
+        assertEquals("appended.value", result.get("appended.key"));
+
+        File[] leftovers = tmpDir.listFiles((dir, name) -> 
name.endsWith(".tmp"));
+        assertTrue("no temporary file must be left behind after the atomic 
rename",
+                Objects.requireNonNull(leftovers).length == 0);
+    }
+
 }

Reply via email to