diff --git a/src/Shared/Internal/ReEntrantScopeLock.cs b/src/Shared/Internal/ReEntrantScopeLock.cs new file mode 100644 index 00000000..67bf30c7 --- /dev/null +++ b/src/Shared/Internal/ReEntrantScopeLock.cs @@ -0,0 +1,100 @@ +using System; +#if !NET35 +using System.Threading; +#endif +#if ASP_NET_CORE +using HttpContextBase = Microsoft.AspNetCore.Http.HttpContext; +#else +using System.Web; +#endif + +namespace NLog.Web.Internal +{ + /// + /// Manages if a LayoutRenderer can be called recursively. + /// Especially used by AspNetSessionValueLayoutRenderer + /// + /// Since NET 35 does not support AsyncLocal or even ThreadLocal + /// a different technique must be used for that platform + /// + internal readonly struct ReEntrantScopeLock : IDisposable + { + internal ReEntrantScopeLock(HttpContextBase context) + { + _httpContext = context; + IsLockAcquired = TryGetLock(context); + } + + private readonly HttpContextBase _httpContext; + + // Need to track if we were successful in the lock + // If we were not, we should not unlock in the dispose code + internal bool IsLockAcquired { get; } + +#if NET35 + private static readonly object ReEntrantLock = new object(); + + private static bool TryGetLock(HttpContextBase context) + { + // If context is null leave + if (context == null) + { + return false; + } + + // If already locked, return false + if (context.Items?.Contains(ReEntrantLock) == true) + { + return false; + } + + // Get the lock + context.Items[ReEntrantLock] = bool.TrueString; + + // Indicate the lock was successfully acquired + return true; + } + + public void Dispose() + { + // Only unlock if we were the ones who locked it + if (IsLockAcquired) + { + _httpContext.Items?.Remove(ReEntrantLock); + } + } +#else + private static readonly AsyncLocal ReEntrantLock = new AsyncLocal(); + + private static bool TryGetLock(HttpContextBase context) + { + // If context is null leave + if (context == null) + { + return false; + } + + // If already locked, return false + if (ReEntrantLock.Value) + { + return false; + } + + // Get the lock + ReEntrantLock.Value = true; + + // Indicate the lock was successfully acquired + return true; + } + + public void Dispose() + { + // Only unlock if we were the ones who locked it + if (IsLockAcquired) + { + ReEntrantLock.Value = false; + } + } +#endif + } +} diff --git a/src/Shared/LayoutRenderers/AspNetSessionValueLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetSessionValueLayoutRenderer.cs index 9774f0e4..dbde34e9 100644 --- a/src/Shared/LayoutRenderers/AspNetSessionValueLayoutRenderer.cs +++ b/src/Shared/LayoutRenderers/AspNetSessionValueLayoutRenderer.cs @@ -1,13 +1,13 @@ -using System; using System.Globalization; using System.Text; using NLog.Config; using NLog.LayoutRenderers; using NLog.Web.Internal; -#if !ASP_NET_CORE -using System.Web; -#else +using NLog.Common; +#if ASP_NET_CORE using Microsoft.AspNetCore.Http; +#else +using System.Web; #endif namespace NLog.Web.LayoutRenderers @@ -40,10 +40,6 @@ namespace NLog.Web.LayoutRenderers [LayoutRenderer("aspnet-session")] public class AspNetSessionValueLayoutRenderer : AspNetLayoutRendererBase { -#if ASP_NET_CORE - private static readonly object NLogRetrievingSessionValue = new object(); -#endif - /// /// Gets or sets the session item name. /// @@ -78,7 +74,7 @@ public class AspNetSessionValueLayoutRenderer : AspNetLayoutRendererBase #if ASP_NET_CORE /// - /// The hype of the value. + /// The type of the value. /// public SessionValueType ValueType { get; set; } = SessionValueType.String; #endif @@ -93,36 +89,36 @@ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) } var context = HttpContextAccessor.HttpContext; - var contextSession = context.TryGetSession(); - if (contextSession == null) - return; - -#if !ASP_NET_CORE - var value = PropertyReader.GetValue(item, contextSession, (session,key) => session.Count > 0 ? session[key] : null, EvaluateAsNestedProperties); -#else - //because session.get / session.getstring also creating log messages in some cases, this could lead to stackoverflow issues. - //We remember on the context.Items that we are looking up a session value so we prevent stackoverflows - if (context.Items == null || (context.Items.Count > 0 && context.Items.ContainsKey(NLogRetrievingSessionValue))) + if (context == null) { return; } - context.Items[NLogRetrievingSessionValue] = bool.TrueString; - - object value; - try - { - value = PropertyReader.GetValue(item, contextSession, (session, key) => GetSessionValue(session, key), EvaluateAsNestedProperties); - } - finally + var contextSession = context?.TryGetSession(); + if (contextSession == null) { - context.Items.Remove(NLogRetrievingSessionValue); + return; } -#endif - if (value != null) + + // Because session.get / session.getstring are also creating log messages in some cases, + // this could lead to stack overflow issues. + // We remember that we are looking up a session value so we prevent stack overflows + using (var reEntry = new ReEntrantScopeLock(context)) { - var formatProvider = GetFormatProvider(logEvent, Culture); - builder.AppendFormattedValue(value, Format, formatProvider, ValueFormatter); + if (!reEntry.IsLockAcquired) + { + InternalLogger.Debug( + "Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException."); + return; + } + + var value = PropertyReader.GetValue(item, contextSession, GetSessionValue, EvaluateAsNestedProperties); + + if (value != null) + { + var formatProvider = GetFormatProvider(logEvent, Culture); + builder.AppendFormattedValue(value, Format, formatProvider, ValueFormatter); + } } } @@ -133,9 +129,16 @@ private object GetSessionValue(ISession session, string key) { case SessionValueType.Int32: return session.GetInt32(key); - default: return session.GetString(key); + default: + return session.GetString(key); } } +#else + private object GetSessionValue(HttpSessionStateBase session, string key) + { + return session.Count == 0 ? null : session[key]; + } #endif + } } \ No newline at end of file