paulrutter commented on code in PR #544:
URL: https://github.com/apache/felix-dev/pull/544#discussion_r4013209468


##########
fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java:
##########
@@ -433,6 +444,17 @@ boolean deleteConfig(File f) throws Exception
     {
         String pid[] = parsePid(f.getName());
         Configuration config = getConfiguration(toConfigKey(f), pid[0], 
pid[1]);
+
+        // Only delete if this file is the registered source of the 
configuration.
+        // If the registered felix.fileinstall.filename does not match the 
file being deleted, skip deletion to protect the live config.
+        Dictionary<String, Object> props = config.getProperties();
+        if (props != null) {
+            String registeredFileName = (String) 
props.get(DirectoryWatcher.FILENAME);
+            if (registeredFileName != null && 
!registeredFileName.equals(toConfigKey(f))) {

Review Comment:
   **Comparing the two URIs as raw strings instead of as file identities makes 
the guard fire on paths that denote the same file.**
   
   `toConfigKey()` is `f.getAbsoluteFile().toURI().toString()`, and 
`getAbsoluteFile()` does not normalize `.`/`..` segments or (on Windows) 
drive-letter case.
   
   Scenario: an admin initially sets `felix.fileinstall.dir=./etc`, so the 
configuration is stored with 
`felix.fileinstall.filename=file:/opt/app/./etc/app.cfg`. Later they tidy the 
property to `felix.fileinstall.dir=/opt/app/etc`. After restart, 
`toConfigKey()` yields `file:/opt/app/etc/app.cfg`, which is `!equals` the 
stored value even though it is the very same file. From then on `setConfig()` 
silently returns `false` — edits to `app.cfg` never reach ConfigAdmin — and 
`deleteConfig()` refuses to remove the configuration when the file is deleted. 
Windows `c:/watched` vs `C:/watched` is the same failure class.
   
   Comparing resolved files rather than strings avoids this:
   
   ```java
   String registeredFileName = (String) props.get(DirectoryWatcher.FILENAME);
   if (registeredFileName != null
           && 
!fromConfigKey(registeredFileName).getAbsoluteFile().equals(f.getAbsoluteFile()))
 {
       return false;
   }
   ```
   
   (`File.equals` applies the platform's case rules; `getCanonicalFile()` would 
additionally resolve symlinks, at the cost of an I/O call.)
   



##########
fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java:
##########
@@ -388,6 +388,17 @@ boolean setConfig(final File f) throws Exception
         clearReadOnlyIfWritable(pid, config, f);
 
         Dictionary<String, Object> props = config.getProperties();
+
+        // Only update if this file is the registered source of the 
configuration,
+        // or if no source has been registered yet (new configuration).
+        // Skips duplicate files that share the same PID but originate from a 
different path.
+        if (props != null) {

Review Comment:
   **The early return sits after `clearReadOnlyIfWritable()` but before the 
`try`/`finally` that calls `setReadOnlyInNotWritable()`, so it leaks the 
READ_ONLY attribute of the *live* configuration.**
   
   Scenario: `/watched/etc/app.cfg` is read-only, so the live configuration for 
PID `app` carries the READ_ONLY attribute. A *writable* duplicate 
`/watched/backup/app.cfg` is then dropped into a subdirectory. 
`clearReadOnlyIfWritable(pid, config, f)` (line 388) sees 
`Util.canWrite(duplicate) == true` and strips READ_ONLY from the live config. 
The new check then returns `false`, so the `finally { 
setReadOnlyInNotWritable(...) }` block is never reached and the attribute is 
never restored. The live configuration is left permanently writable — exactly 
the kind of cross-file interference this PR sets out to prevent.
   
   The check needs to run *before* `clearReadOnlyIfWritable`:
   
   ```suggestion
           Dictionary<String, Object> props = config.getProperties();
   
           // Only update if this file is the registered source of the 
configuration,
           // or if no source has been registered yet (new configuration).
           // Skips duplicate files that share the same PID but originate from 
a different path.
           if (props != null) {
               String registeredFileName = (String) 
props.get(DirectoryWatcher.FILENAME);
               if (registeredFileName != null && 
!registeredFileName.equals(toConfigKey(f))) {
                   Util.log(context, Logger.LOG_WARNING, "Skipping 
configuration update for "
                           + f.getAbsolutePath() + ": PID {" + config.getPid()
                           + "} is already owned by " + registeredFileName, 
null);
                   return false;
               }
           }
   
           clearReadOnlyIfWritable(pid, config, f);
   ```
   



##########
fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java:
##########
@@ -388,6 +388,17 @@ boolean setConfig(final File f) throws Exception
         clearReadOnlyIfWritable(pid, config, f);
 
         Dictionary<String, Object> props = config.getProperties();
+
+        // Only update if this file is the registered source of the 
configuration,
+        // or if no source has been registered yet (new configuration).
+        // Skips duplicate files that share the same PID but originate from a 
different path.
+        if (props != null) {
+            String registeredFileName = (String) 
props.get(DirectoryWatcher.FILENAME);
+            if (registeredFileName != null && 
!registeredFileName.equals(toConfigKey(f))) {
+                return false;
+            }
+        }

Review Comment:
   **The skip is permanent and never retried, so a legitimate file move 
detected across two scan cycles loses the configuration for good.**
   
   `DirectoryWatcher.install(Artifact)` discards the return value of 
`ArtifactInstaller.install()` and then unconditionally calls `setArtifact(path, 
artifact)`, recording the file and its checksum as successfully installed 
(`DirectoryWatcher.java:940-972`). So once `setConfig()` returns `false` here, 
the file will never be re-offered until its bytes change.
   
   Scenario (`cp` then `rm`, one scan cycle apart — 2s default poll, so easy to 
hit):
   1. Cycle N: `/watched/sub/app.cfg` appears. `setConfig()` sees PID `app` 
owned by `/watched/etc/app.cfg`, returns `false`. DirectoryWatcher still 
records `sub/app.cfg` + checksum as installed.
   2. Cycle N+1: `/watched/etc/app.cfg` is deleted. `deleteConfig()` matches 
the registered filename and deletes the configuration.
   3. `sub/app.cfg` is still on disk, unchanged, and is already in 
`currentManagedArtifacts` — it is never re-processed. The configuration is gone 
permanently and no restart-free recovery exists.
   
   Before this patch the config survived step 2 (re-created from the surviving 
file on the next change). Same root cause produces a permanently orphaned 
`.cfg` whenever a duplicate happens to be scanned *first* on startup (directory 
iteration order decides which file wins, and the loser is locked out silently).
   
   Consider recording rejected paths and re-evaluating them when the owning 
configuration disappears (`configurationEvent` already handles `CM_DELETED`), 
or routing the rejection through `DirectoryWatcher.processingFailures` so it 
gets retried.
   



##########
fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java:
##########
@@ -433,6 +444,17 @@ boolean deleteConfig(File f) throws Exception
     {
         String pid[] = parsePid(f.getName());
         Configuration config = getConfiguration(toConfigKey(f), pid[0], 
pid[1]);
+
+        // Only delete if this file is the registered source of the 
configuration.
+        // If the registered felix.fileinstall.filename does not match the 
file being deleted, skip deletion to protect the live config.
+        Dictionary<String, Object> props = config.getProperties();
+        if (props != null) {
+            String registeredFileName = (String) 
props.get(DirectoryWatcher.FILENAME);
+            if (registeredFileName != null && 
!registeredFileName.equals(toConfigKey(f))) {
+                return false;
+            }
+        }

Review Comment:
   **Silent skip: this is the only exit from `deleteConfig()` that logs 
nothing.**
   
   Every other outcome in `setConfig()`/`deleteConfig()` emits a 
`Util.log(...)` line ("Creating/Updating/Deleting configuration ..."). When 
this guard triggers, an admin sees a `.cfg` file being created or deleted in a 
watched directory with no effect on ConfigAdmin and nothing at all in the log 
to explain why — which makes the duplicate-PID situation the patch detects 
effectively undiagnosable in production. Please log at WARNING with the file 
path, the PID, and the registered owner.
   
   Also, the javadoc above still reads `@return <code>true</code>`, which is no 
longer accurate now that `false` is reachable.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to