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

FreeAndNil pushed a commit to branch Feature/security-audit-hardening
in repository https://gitbox.apache.org/repos/asf/logging-log4net.git

commit ecd1b8b9e26c8b4a5414d67c757b1ffc692b1355
Author: Jan Friedrich <[email protected]>
AuthorDate: Mon Aug 17 21:29:19 2026 +0200

    report the first appender error without log4net.Internal.Debug
    
    OnlyOnceErrorHandler.FirstError only forwarded to LogLog when
    LogLog.InternalDebugging was set, which is off by default. Since every
    appender uses this handler by default, an appender that stopped
    delivering events did so completely silently: no stderr line and no
    LogLog.LogReceived event, and the handler disables itself afterwards.
    
    The condition was redundant anyway: LogLog.Error already checks
    LogLog.QuietMode (log4net.Internal.Quiet) and EmitInternalMessages, so
    both documented ways of silencing internal messages keep working.
    
    Upgrade note: previously invisible appender errors are now visible, so a
    misconfigured appender emits one log4net:ERROR line on stderr.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../309-report-first-appender-error-by-default.xml |  14 +++
 src/log4net.Tests/Util/OnlyOnceErrorHandlerTest.cs | 121 +++++++++++++++++++++
 src/log4net/Util/OnlyOnceErrorHandler.cs           |   9 +-
 3 files changed, 140 insertions(+), 4 deletions(-)

diff --git a/src/changelog/3.4.0/309-report-first-appender-error-by-default.xml 
b/src/changelog/3.4.0/309-report-first-appender-error-by-default.xml
new file mode 100644
index 00000000..6d372d61
--- /dev/null
+++ b/src/changelog/3.4.0/309-report-first-appender-error-by-default.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="https://logging.apache.org/xml/ns";
+       xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="changed">
+  <issue id="309" link="https://github.com/apache/logging-log4net/pull/309"/>
+  <description format="asciidoc">
+    report the first error of an appender even when `log4net.Internal.Debug` 
is off, which is the
+ default. `OnlyOnceErrorHandler` is the default error handler of every 
appender, so an appender
+ that stopped delivering events previously did so without leaving any trace 
(CWE-778).
+ `log4net.Internal.Quiet` and `LogLog.EmitInternalMessages` remain the ways to 
silence internal
+ messages (audit 1231d72-f019)
+  </description>
+</entry>
diff --git a/src/log4net.Tests/Util/OnlyOnceErrorHandlerTest.cs 
b/src/log4net.Tests/Util/OnlyOnceErrorHandlerTest.cs
new file mode 100644
index 00000000..a2731edb
--- /dev/null
+++ b/src/log4net.Tests/Util/OnlyOnceErrorHandlerTest.cs
@@ -0,0 +1,121 @@
+#region Apache License
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to you under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System.Collections.Generic;
+
+using log4net.Util;
+
+using NUnit.Framework;
+
+namespace log4net.Tests.Util;
+
+/// <summary>
+/// Used for internal unit testing the <see cref="OnlyOnceErrorHandler"/> 
class.
+/// </summary>
+[TestFixture]
+public class OnlyOnceErrorHandlerTest
+{
+  /// <summary>
+  /// The first error must reach <see cref="LogLog"/> even when
+  /// <see cref="LogLog.InternalDebugging"/> is off, which is the default. An 
appender that
+  /// stops delivering events would otherwise leave no trace at all.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void FirstErrorIsEmittedWithoutInternalDebugging()
+  {
+    bool internalDebugging = LogLog.InternalDebugging;
+    LogLog.InternalDebugging = false;
+    try
+    {
+      List<LogLog> messages = [];
+      LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+      {
+        using LogLog.LogReceivedAdapter _ = new(messages);
+        new OnlyOnceErrorHandler("TestAppender").Error("Something went wrong");
+      });
+
+      Assert.That(messages, Has.Count.EqualTo(1));
+      Assert.That(messages[0].Message, Does.Contain("Something went wrong"));
+    }
+    finally
+    {
+      LogLog.InternalDebugging = internalDebugging;
+    }
+  }
+
+  /// <summary>
+  /// Only the first error is reported: the handler disables itself afterwards 
so that a
+  /// repeatedly failing appender cannot flood the internal log.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void OnlyTheFirstErrorIsEmitted()
+  {
+    bool internalDebugging = LogLog.InternalDebugging;
+    LogLog.InternalDebugging = false;
+    try
+    {
+      List<LogLog> messages = [];
+      OnlyOnceErrorHandler handler = new("TestAppender");
+      LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+      {
+        using LogLog.LogReceivedAdapter _ = new(messages);
+        handler.Error("First failure");
+        handler.Error("Second failure");
+        handler.Error("Third failure");
+      });
+
+      Assert.That(messages, Has.Count.EqualTo(1));
+      Assert.That(messages[0].Message, Does.Contain("First failure"));
+      Assert.That(handler.IsEnabled, Is.False);
+    }
+    finally
+    {
+      LogLog.InternalDebugging = internalDebugging;
+    }
+  }
+
+  /// <summary>
+  /// <see cref="LogLog.QuietMode"/> (the <c>log4net.Internal.Quiet</c> 
setting) remains the
+  /// documented way to silence internal messages, including appender errors.
+  /// </summary>
+  [Test]
+  [NonParallelizable]
+  public void QuietModeSuppressesTheError()
+  {
+    bool quietMode = LogLog.QuietMode;
+    LogLog.QuietMode = true;
+    try
+    {
+      List<LogLog> messages = [];
+      LogLog.ExecuteWithoutEmittingInternalMessages(() =>
+      {
+        using LogLog.LogReceivedAdapter _ = new(messages);
+        new OnlyOnceErrorHandler("TestAppender").Error("Something went wrong");
+      });
+
+      Assert.That(messages, Is.Empty);
+    }
+    finally
+    {
+      LogLog.QuietMode = quietMode;
+    }
+  }
+}
diff --git a/src/log4net/Util/OnlyOnceErrorHandler.cs 
b/src/log4net/Util/OnlyOnceErrorHandler.cs
index d0c9ebe2..909529b2 100644
--- a/src/log4net/Util/OnlyOnceErrorHandler.cs
+++ b/src/log4net/Util/OnlyOnceErrorHandler.cs
@@ -114,10 +114,11 @@ public virtual void FirstError(string message, Exception? 
e, ErrorCode errorCode
     ErrorMessage = message;
     IsEnabled = false;
 
-    if (LogLog.InternalDebugging && !LogLog.QuietMode)
-    {
-      LogLog.Error(_declaringType, "[" + _prefix + "] ErrorCode: " + 
errorCode.ToString() + ". " + message, e);
-    }
+    // Emit the first error unconditionally so that an appender which silently 
stopped
+    // delivering events leaves a trace in a default configuration. 
LogLog.Error already
+    // honors LogLog.QuietMode (log4net.Internal.Quiet) and 
LogLog.EmitInternalMessages,
+    // which remain the documented ways to silence internal messages.
+    LogLog.Error(_declaringType, "[" + _prefix + "] ErrorCode: " + 
errorCode.ToString() + ". " + message, e);
   }
 
   /// <summary>

Reply via email to