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 2fb4539f5c78f7037061265346f3992f54bcad67
Author: Jan Friedrich <[email protected]>
AuthorDate: Mon Aug 17 21:42:58 2026 +0200

    time out writes to stalled TelnetAppender clients
    
    Clients are written to synchronously while the appender lock is held and no
    Socket.SendTimeout was set anywhere, so a client that connects and then
    stops reading let TCP flow control fill its receive window and the server
    send buffer. The next write blocked forever and every thread logging
    through the appender queued behind it. The existing eviction only fires on
    a thrown exception, and a blocked write never throws.
    
    Accepted sockets now get a finite SendTimeout, configurable through the new
    SendTimeoutMillis property and defaulting to 5000. A timed-out write throws
    and the client is evicted like any other dead connection. Setting the
    property to 0 restores the previous unbounded behavior.
    
    SocketHandler gained a (port, sendTimeoutMillis) overload rather than an
    optional parameter, so the existing (port) signature keeps working for
    subclasses; it maps to 0 to preserve its old semantics.
    
    Writes stay synchronous under the appender lock, so several stalled clients
    still cost up to the timeout each. Moving the sends to a bounded per-client
    queue would remove that entirely and is left as a follow-up.
    
    TelnetAppender had no page in the manual, which is added here, including
    that it is a diagnostic tool for trusted networks and that the connecting
    client is trusted, like any other appender destination.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../3.4.0/309-telnet-appender-send-timeout.xml     | 15 ++++
 src/log4net.Tests/Appender/TelnetAppenderTest.cs   | 31 ++++++++
 src/log4net/Appender/TelnetAppender.cs             | 74 +++++++++++++++++-
 src/site/antora/modules/ROOT/nav.adoc              |  1 +
 .../ROOT/pages/manual/configuration/appenders.adoc |  3 +-
 .../configuration/appenders/telnetappender.adoc    | 88 ++++++++++++++++++++++
 6 files changed, 209 insertions(+), 3 deletions(-)

diff --git a/src/changelog/3.4.0/309-telnet-appender-send-timeout.xml 
b/src/changelog/3.4.0/309-telnet-appender-send-timeout.xml
new file mode 100644
index 00000000..e5813cc7
--- /dev/null
+++ b/src/changelog/3.4.0/309-telnet-appender-send-timeout.xml
@@ -0,0 +1,15 @@
+<?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="fixed">
+  <issue id="309" link="https://github.com/apache/logging-log4net/pull/309"/>
+  <description format="asciidoc">
+    stop a Telnet client that connects and then stops reading from suspending 
all logging.
+ `TelnetAppender` writes to its clients while the appender lock is held and 
set no
+ `Socket.SendTimeout`, so once TCP flow control filled the client's receive 
window the next write
+ blocked forever and every thread logging through the appender queued behind 
it (CWE-833). Writes
+ now time out after `SendTimeoutMillis` (5000 by default) and the client is 
disconnected; 0
+ restores the previous unbounded behavior (audit 1231d72-f001)
+  </description>
+</entry>
diff --git a/src/log4net.Tests/Appender/TelnetAppenderTest.cs 
b/src/log4net.Tests/Appender/TelnetAppenderTest.cs
index e8cf95ec..cde2231d 100644
--- a/src/log4net.Tests/Appender/TelnetAppenderTest.cs
+++ b/src/log4net.Tests/Appender/TelnetAppenderTest.cs
@@ -129,6 +129,37 @@ void WaitForReceived(string what, string expected)
     }
   }
 
+  /// <summary>
+  /// Writes to a client block while the appender lock is held, so the send 
timeout has to be
+  /// finite by default - otherwise a client that stops reading suspends all 
logging.
+  /// </summary>
+  [Test]
+  public void SendTimeoutMillisDefaultsToAFiniteValue()
+  {
+    TelnetAppender appender = new();
+
+    Assert.That(appender.SendTimeoutMillis, Is.EqualTo(5000));
+  }
+
+  /// <summary>
+  /// 0 is the documented opt-out that restores blocking indefinitely; a 
negative timeout has no
+  /// meaning for <see cref="Socket.SendTimeout"/> and is rejected instead of 
being silently
+  /// reinterpreted.
+  /// </summary>
+  [Test]
+  public void SendTimeoutMillisRejectsNegativeValuesButAllowsZero()
+  {
+    TelnetAppender appender = new();
+
+    Assert.That(() => appender.SendTimeoutMillis = -1, 
Throws.TypeOf<ArgumentOutOfRangeException>());
+
+    appender.SendTimeoutMillis = 0;
+    Assert.That(appender.SendTimeoutMillis, Is.EqualTo(0));
+
+    appender.SendTimeoutMillis = 250;
+    Assert.That(appender.SendTimeoutMillis, Is.EqualTo(250));
+  }
+
   /// <summary>
   /// Asks the OS for a currently unused TCP port - a fixed port would collide 
with
   /// other tests or processes on the build machine.
diff --git a/src/log4net/Appender/TelnetAppender.cs 
b/src/log4net/Appender/TelnetAppender.cs
index adb4cdae..e62e9346 100644
--- a/src/log4net/Appender/TelnetAppender.cs
+++ b/src/log4net/Appender/TelnetAppender.cs
@@ -40,6 +40,13 @@ namespace log4net.Appender;
 /// <para>
 /// The default <see cref="Port"/> is 23 (the telnet port).
 /// </para>
+/// <para>
+/// This appender is a diagnostic tool for trusted networks. As with any other 
appender
+/// destination, the connecting client is trusted: enabling the appender 
declares that whoever can
+/// reach the port may read the application's log, so no authentication is 
performed and the stream
+/// is not encrypted. Keeping untrusted parties away from the port is the 
operator's
+/// responsibility, exactly as it is for a log file.
+/// </para>
 /// </remarks>
 /// <author>Keith Long</author>
 /// <author>Nicko Cadell</author>
@@ -47,6 +54,7 @@ public class TelnetAppender : AppenderSkeleton
 {
   private SocketHandler? _handler;
   private int _listeningPort = 23;
+  private int _sendTimeoutMillis = 5_000;
 
   /// <summary>
   /// The fully qualified type of the TelnetAppender class.
@@ -85,6 +93,42 @@ public int Port
     }
   }
 
+  /// <summary>
+  /// Gets or sets the time, in milliseconds, that a write to a client may 
block before that
+  /// client is treated as dead and disconnected.
+  /// </summary>
+  /// <value>
+  /// A positive number of milliseconds, or 0 to block indefinitely.
+  /// </value>
+  /// <remarks>
+  /// <para>
+  /// Clients are written to synchronously while the appender lock is held, so 
a client that
+  /// connects and then stops reading lets TCP flow control fill its receive 
window and the
+  /// server send buffer. Without a timeout the next write blocks forever and 
suspends every
+  /// thread that logs through this appender.
+  /// </para>
+  /// <para>
+  /// The default value is 5000 (5 seconds). A write that exceeds it fails 
with a
+  /// <see cref="SocketException"/>, and the client is then disconnected like 
any other dead
+  /// connection. Setting the value to 0 restores the previous behavior of 
blocking
+  /// indefinitely and is not recommended.
+  /// </para>
+  /// </remarks>
+  /// <exception cref="ArgumentOutOfRangeException">The value specified is 
negative.</exception>
+  public int SendTimeoutMillis
+  {
+    get => _sendTimeoutMillis;
+    set
+    {
+      if (value < 0)
+      {
+        throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), 
value,
+          "The value specified for SendTimeoutMillis is negative.");
+      }
+      _sendTimeoutMillis = value;
+    }
+  }
+
   /// <summary>
   /// Overrides the parent method to close the socket handler
   /// </summary>
@@ -115,7 +159,7 @@ public override void ActivateOptions()
     try
     {
       LogLog.Debug(_declaringType, $"Creating SocketHandler to listen on port 
[{_listeningPort}]");
-      _handler = new SocketHandler(_listeningPort);
+      _handler = new SocketHandler(_listeningPort, _sendTimeoutMillis);
     }
     catch (Exception ex)
     {
@@ -151,6 +195,7 @@ protected class SocketHandler : IDisposable
     private const int MaxConnections = 20;
 
     private readonly Socket _serverSocket;
+    private readonly int _sendTimeoutMillis;
     private readonly List<SocketClient> _clients = [];
     private readonly object _syncRoot = new();
     private bool _wasDisposed;
@@ -238,11 +283,28 @@ public void Dispose()
     /// <param name="port">the local port to listen on for connections</param>
     /// <remarks>
     /// <para>
-    /// Creates a socket handler on the specified local server port.
+    /// Creates a socket handler on the specified local server port, blocking 
indefinitely on
+    /// clients that stop reading. Prefer <see cref="SocketHandler(int, 
int)"/>.
     /// </para>
     /// </remarks>
     public SocketHandler(int port)
+      : this(port, 0)
+    { }
+
+    /// <summary>
+    /// Opens a new server port on <paramref ref="port"/>
+    /// </summary>
+    /// <param name="port">the local port to listen on for connections</param>
+    /// <param name="sendTimeoutMillis">the time, in milliseconds, that a 
write to a client may
+    /// block before that client is disconnected, or 0 to block 
indefinitely</param>
+    /// <remarks>
+    /// <para>
+    /// Creates a socket handler on the specified local server port.
+    /// </para>
+    /// </remarks>
+    public SocketHandler(int port, int sendTimeoutMillis)
     {
+      _sendTimeoutMillis = sendTimeoutMillis;
       _serverSocket = new(AddressFamily.InterNetwork, SocketType.Stream, 
ProtocolType.Tcp);
       _serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
       _serverSocket.Listen(5);
@@ -332,6 +394,14 @@ private void OnConnect(IAsyncResult asyncResult)
         // Block until a client connects
         Socket socket = _serverSocket.EndAccept(asyncResult);
         LogLog.Debug(_declaringType, $"Accepting connection from 
[{socket.RemoteEndPoint}]");
+        if (_sendTimeoutMillis > 0)
+        {
+          // Bound how long a write to this client can block. Clients are 
written to while the
+          // appender lock is held, so without a timeout a client that stops 
reading suspends
+          // every thread that logs. A timed-out write throws and the client 
is then evicted
+          // like any other dead connection.
+          socket.SendTimeout = _sendTimeoutMillis;
+        }
         SocketClient client = new(socket);
 
         // clients.Count is an atomic read that can be done outside the lock.
diff --git a/src/site/antora/modules/ROOT/nav.adoc 
b/src/site/antora/modules/ROOT/nav.adoc
index 8aadc37d..c6f993ee 100644
--- a/src/site/antora/modules/ROOT/nav.adoc
+++ b/src/site/antora/modules/ROOT/nav.adoc
@@ -38,6 +38,7 @@
 **** xref:manual/configuration/appenders/rollingfileappender.adoc[]
 **** xref:manual/configuration/appenders/smtpappender.adoc[]
 **** xref:manual/configuration/appenders/smtppickupdirappender.adoc[]
+**** xref:manual/configuration/appenders/telnetappender.adoc[]
 **** xref:manual/configuration/appenders/traceappender.adoc[]
 **** xref:manual/configuration/appenders/udpappender.adoc[]
 *** xref:manual/configuration/filters.adoc[]
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
index e2e0c18a..d5cf4bae 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders.adoc
@@ -97,8 +97,9 @@ The MailKit based appender from the `log4net.Ext.Mail` 
package is recommended; t
 |xref:manual/configuration/appenders/smtppickupdirappender.adoc[]
 |Sends logging events to an email address but writes the emails to a 
configurable directory rather than sending them directly via SMTP.
 
-|TelnetAppender
+|xref:manual/configuration/appenders/telnetappender.adoc[]
 |*Clients* connect via Telnet to receive logging events.
+The connection is unauthenticated and unencrypted.
 
 |xref:manual/configuration/appenders/traceappender.adoc[]
 |Writes logging events to the .NET trace system 
(https://web.archive.org/web/20240907024634/https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.trace?view=net-8.0[System.Diagnostics.Trace]).
diff --git 
a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc
 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc
new file mode 100644
index 00000000..27891b57
--- /dev/null
+++ 
b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/telnetappender.adoc
@@ -0,0 +1,88 @@
+////
+    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.
+////
+
+[#telnetappender]
+= TelnetAppender
+
+The `TelnetAppender` listens for incoming TCP connections and streams rendered 
log events to
+every connected client, so that a running application's log can be watched 
over a socket with a
+telnet client.
+Unlike every other appender, it does not write to a destination you configure: 
it accepts
+connections from clients that reach it.
+It is intended for diagnostic use on trusted networks -- see 
<<telnetappender-trust>>.
+
+At most 20 clients may be connected at the same time; further connection 
attempts are answered
+with a message and closed.
+
+The following example configures the appender to listen on port 8023.
+
+[source,xml]
+----
+<appender name="TelnetAppender" type="log4net.Appender.TelnetAppender">
+  <port value="8023" />
+  <sendTimeoutMillis value="5000" />
+  <layout type="log4net.Layout.PatternLayout">
+    <conversionPattern value="%date %-5level %logger - %message%newline" />
+  </layout>
+</appender>
+----
+
+[#telnetappender-settings]
+== Settings
+
+`port`::
+The TCP port to listen on.
+The default is `23`, the telnet port.
+
+`sendTimeoutMillis`::
+How long, in milliseconds, a write to a client may block before that client is 
treated as dead
+and disconnected.
+The default is `5000`.
++
+Clients are written to synchronously while the appender lock is held, so a 
client that connects
+and then stops reading lets TCP flow control fill its receive window.
+A finite timeout bounds how long that client can hold up the threads that log 
through this
+appender.
+Setting the value to `0` restores blocking indefinitely and is not recommended.
+
+[#telnetappender-trust]
+== Intended use and trust model
+
+This appender is a *diagnostic tool for trusted networks*.
+It is meant for watching the log of a running application during development 
or while
+investigating a problem, not as a general-purpose logging destination.
+
+Like every other appender destination, the connecting client is *trusted*: by 
enabling the
+appender the operator declares that whoever can reach the port is allowed to 
read the
+application's log.
+The appender therefore performs no authentication of its own.
+
+[WARNING]
+====
+The connection is *unauthenticated* and *unencrypted*, and the appender 
listens on *all network
+interfaces*.
+There is no option to restrict the listen address, require a credential, or 
enable TLS.
+
+Any client that can reach the port receives the full rendered log stream, 
including whatever the
+layout renders -- user names, session identifiers, request parameters, stack 
traces.
+Keeping untrusted parties away from the port is the operator's responsibility, 
exactly as it is
+for a log file:
+
+* Only enable this appender on a trusted network.
+* Restrict access to the port with a host firewall or network policy.
+* Prefer it for local or short-lived diagnostics rather than as a permanent 
logging destination.
+====

Reply via email to