Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 2개 있습니다.)
.NET Framework: 786. ASP.NET - HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상
; https://www.sysnet.pe.kr/2/0/11587

디버깅 기술: 221. WinDbg 분석 사례 - ASP.NET HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/13916




ASP.NET - HttpCookieCollection을 다중 스레드에서 사용할 경우 무한 루프 현상

HttpCookieCollection에는 HashSet<string> 타입의 _keysAwaitingValidation 필드를 가지고 있습니다. 대부분의 Set 자료 구조가 그렇듯이 HashSet<T>도 hash 기반의 bucket과 연결 리스트로 구성되어 있습니다.

당연히, HttpCookieCollection은 thread-safe한 타입이 아니며, 그 내부에서 사용하고 있는 HashSet<T> 역시 thread-safe하지 않습니다. 따라서, 다중 스레드에서 HttpCookieCollection을 접근한다면 HttpCookieCollection은 물론이고 내부에서 필드로 사용된 타입들의 안정성 역시 보장할 수 없습니다.

그렇긴 한데, 사실 HttpCookieCollection의 경우 ASP.NET에서 사용 시 HttpContext.Current.Request나 HttpContext.Current.Response를 통해 사용하는 것이 보통입니다. 그리고 대부분의 경우 Request를 통해 접근한다면 이미 ASP.NET Pipeline 내부에서 설정이 된 상태로 나오기 때문에 조회는 빈번해도 add/remove는 되지 않으므로 다중 스레드에서 사용해도 안전하게 동작을 합니다.

하지만, asp.net 환경에 따라 HttpCookieCollection의 _keysAwaitingValidation에 삭제 연산이 실행될 때가 있습니다. 바로, web.config에 RequestValidationMode를 .NET 4.5 이상으로 설정한 경우입니다.

<httpRuntime requestValidationMode="4.5" />

HttpRuntimeSection.RequestValidationMode 속성 
; https://learn.microsoft.com/en-us/dotnet/api/system.web.configuration.httpruntimesection.requestvalidationmode

이렇게 되면, asp.net은 쿠키에 대한 검증을 하도록 다음과 같은 코드가 실행됩니다.

// System.Web.HttpRequest
internal void ValidateInputIfRequiredByConfig()
{
    HttpRuntimeSection httpRuntime = RuntimeConfig.GetConfig(this.Context).HttpRuntime;
    ...[생략]...
    Version requestValidationMode = httpRuntime.RequestValidationMode;
    if (requestValidationMode == VersionUtil.Framework00)
    {
        this._flags[-2147483648] = true;
    }
    else if (requestValidationMode >= VersionUtil.Framework40)
    {
        this.ValidateInput();
        if (requestValidationMode >= VersionUtil.Framework45)
        {
            this.EnableGranularRequestValidation();
        }
    }
}

internal void EnableGranularRequestValidation()
{
    this._flags[0x40000000] = true;
}

private bool GranularValidationEnabled => this._flags[0x40000000];

이후, GranularValidationEnabled 여부에 따라 쿠키 값을 검증하는 콜백 메서드를 설정하는 코드가 실행됩니다.

private void ValidateCookieCollection(HttpCookieCollection cc)
{
    if (this.GranularValidationEnabled)
    {
        cc.EnableGranularValidation((key, value) => this.ValidateString(value, key, RequestValidationSource.Cookies));
    }
    else
    {
        int count = cc.Count;
        for (int i = 0; i < count; i++)
        {
            string collectionKey = cc.GetKey(i);
            string str2 = cc.Get(i).Value;
            if (!string.IsNullOrEmpty(str2))
            {
                this.ValidateString(str2, collectionKey, RequestValidationSource.Cookies);
            }
        }
    }
}

왜 "Granular" 검증인지 이후의 코드를 보면 이해가 됩니다. 즉, requestValidationMode가 4.0 이하인 경우에는 무조건 모든 쿠키의 값을 검증하는 코드가 ValidateCookieCollection의 if else 문에 의해 실행됩니다. 하지만, requestValidationMode가 4.5 이상이면 HttpCookieCollection의 EnableGranularValidation 메서드가 실행되면서,

internal void EnableGranularValidation(ValidateStringCallback validationCallback)
{
    this._keysAwaitingValidation = new HashSet<string>(this.Keys.Cast<string>(), StringComparer.OrdinalIgnoreCase);
    this._validationCallback = validationCallback;
}

쿠키 값 검증을 하는 _validationCallback의 설정과 함께 현재 HttpCookieCollection에 있는 쿠키의 Key 값들을 전부 _keysAwaitingValidation HashSet에 담아 놓습니다. 이후, 사용자 코드에서 쿠키를 사용하기 위해 접근할 때마다,

// System.Web.HttpCookieCollection
public HttpCookie Get(string name)
{
    HttpCookie cookie = (HttpCookie) base.BaseGet(name);
    if ((cookie == null) && (this._response != null))
    {
        cookie = new HttpCookie(name);
        this.AddCookie(cookie, true);
        this._response.OnCookieAdd(cookie);
    }
    if (cookie != null)
    {
        this.EnsureKeyValidated(name, cookie.Value);
    }
    return cookie;
}

private void EnsureKeyValidated(string key, string value)
{
    if ((this._keysAwaitingValidation != null) && this._keysAwaitingValidation.Contains(key))
    {
        if (!string.IsNullOrEmpty(value))
        {
            this._validationCallback(key, value);
        }
        this._keysAwaitingValidation.Remove(key);
    }
}

저렇게 쿠키의 값을 검증하는 코드가 실행되고, 일단 한번 검증이 되었으면 _keysAwaitingValidation HashSet에서 제거함으로써 나중에 또 HttpCookie.Get 등으로 접근했을 때 중복 검증하지 않도록 막아줍니다.

꽤나 똑똑하게 작성되어 있다는 것을 알 수 있는데요, 다시 정리하면 requestValidationMode가 4.5 이상일 때 asp.net은 요청으로 들어온 Cookie 값들을 개발자 코드에서 사용하는 항목에 한해 처음 접근 시에만 검증을 하고 있는 것입니다. 이쯤 되면 "Granular"라는 단어가 왜 사용되었는지를 알 수 있습니다.




이에 대한 부작용으로는, (물론 그렇게 사용해서는 안 되지만 무심코) 다중 스레드에서 HttpCookieCollection을 사용하게 되면 데이터 정합성이 깨질 확률이 발생하게 되었다는 점입니다. 즉, requestValidationMode == 4.0 이하일 때는 잘 동작하던 코드가 4.5로 올라가는 순간 _keysAwaitingValidation의 자료 구조가 예측할 수 없게 되어 버린 것입니다.

_keysAwaitingValidation의 자료 구조가 깨졌을 때 어떤 현상이 발생할 수 있는지 다음의 코드로 살짝 재현해 볼 수 있는데,

using System;
using System.Collections.Generic;
using System.Reflection;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            HashSet<int> list = new HashSet<int>();
            list.Add(0);

            MakeCpuHigh(list); // 다중 스레드 접근으로 인한 데이터 정합성이 깨지는 것을 억지로 설정

            Console.WriteLine("CPU is running high");
            Console.WriteLine("Will never end...");

            for (int i = 0; i < 100; i++)
            {
                list.Contains(i);
            }
        }

        private static void MakeCpuHigh(HashSet<int> list)
        {
            Type hashType = list.GetType();

            FieldInfo fi = hashType.GetField("m_slots", BindingFlags.NonPublic | BindingFlags.Instance);
            Array slotArray = fi.GetValue(list) as Array;

            Array.Clear(slotArray, 0, 3);
        }
    }
}

위의 코드를 실행하면 list.Contains 호출 시 무한 루프를 도는 현상이 발생합니다.




참고로, 위의 현상은 실제로 고객사에 발생한 것인데 아직 그 원인이 HttpCookieCollection의 다중 스레드 접근으로 인한 것인지는 확인을 못했습니다. 단지, 메모리 덤프를 분석한 결과 저렇게 HashSet<T>.Contains 메서드 호출에서 무한 루프 현상이 발생했고 HashSet<T> 내부의 데이터 조사에서 무한 루프가 발생할 수밖에 없도록 연결 리스트의 값이 설정되어 있는 것을 발견할 수 있었습니다.

물론, 저 현상이 확률적으로 발생하는 것이기 때문에 그동안 문제의 원인이 어떤 것이었는지 파악할 동기가 없었을 것입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/11/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 121  122  123  124  [125]  126  127  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
10798정성태5/26/201522100기타: 53. 2015년 6월 10일 밤 10시 온라인 세미나 - 새로운 Windows 10 App을 개발하는 방법
10797정성태5/23/201521774VC++: 91. 자식 스레드에 자동 상속되는 TEB의 SubProcessTag 필드파일 다운로드1
10796정성태5/23/201532571오류 유형: 293. SQL Server Management Studio 실행 시 "Cannot find one or more components" 오류
10795정성태5/23/201530670오류 유형: 292. InstallUtil로 .NET 서비스 등록 시 오류 - Operation is not supported. (Exception from HRESULT: 0x80131515). [3]
10794정성태5/22/201525608개발 환경 구성: 267. (무료) 마이크로소프트 온라인 강좌 소개 - 네트워킹 기초 [1]
2925정성태5/14/201525132디버깅 기술: 73. PDB 기호 파일의 경로 구성 방식파일 다운로드1
2924정성태5/14/201528463VS.NET IDE: 100. 비주얼 스튜디오 원격 디버깅 시 'Unknown function' 콜스택이 나온다면?
2923정성태5/12/201587846기타: 52. 도서: 시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지 [17]
2922정성태5/12/201524676오류 유형: 291. ssindex.cmd 실행 시 '...[tfs_collection_url]...' not found in srcsrv.ini 오류 발생
2921정성태5/9/201531071개발 환경 구성: 266. 인텔에서 구현한 최대 절전 모드 기능 - Intel® Rapid Start Technology
2920정성태5/9/201522137오류 유형: 290. 디스크 관리자의 파티션 축소 시, There is not enough space available on the disk(s) to complete this operation.
2919정성태5/9/201522065오류 유형: 289. Error: this template attempted to load component assembly 'NuGet.VisualStudio.Interop, ...'
2918정성태5/9/201540556Windows: 111. 복구(Recovery) 파티션 삭제하는 방법 [3]
2917정성태5/9/201530983오류 유형: 288. .NET Framework 4.6이 설치된 경우 "Intel® Rapid Storage Technology (Intel® RST) RAID Driver"가 설치 안 되는 문제 [5]
2916정성태5/9/201532082오류 유형: 287. 레지스트리 권한 오류 - Cannot edit [Registry key name]: Error writing the value's new contents.
2915정성태5/9/201531206개발 환경 구성: 265. TrustedInstaller 권한으로 프로그램 실행시키는 방법 [11]
2914정성태5/9/201528514DDK: 7. 정식 인증서가 있는 경우 Device Driver 서명하는 방법 [2]
2913정성태4/30/201526273.NET Framework: 511. Build 2015 행사에서 소개된 (맥/리눅스/윈도우 용 무료) Visual Studio Code 개발 도구 [8]
2912정성태4/29/201521999오류 유형: 286. VirtualBox에 Windows 8/2012 설치 시 "Error Code: 0x000000C4" 오류 발생
2911정성태4/29/201520668오류 유형: 285. Visual Studio 2015를 제거한 경우 Microsoft.VisualStudio.Web.PageInspector.Loader 어셈블리를 못 찾는 문제 [2]
2910정성태4/29/201524583오류 유형: 284. System.TypeLoadException: Could not load type 'System.Reflection.AssemblySignatureKeyAttribute' from assembly [1]
2909정성태4/29/201520736오류 유형: 283. WCF 연결 오류 - Expected record type 'PreambleAck'
2908정성태4/29/201529010오류 유형: 282. 원격에서 SQL 서버는 연결되지만, SQL Express는 연결되지 않는 경우
2907정성태4/29/201519093.NET Framework: 510. 제네릭(Generic) 인자에 대한 메타데이터 등록 확인
2906정성태4/28/201521629오류 유형: 281. DebugView로 인한 System.Diagnostics.Trace.WriteLine 멈춤(Hang) 현상
2905정성태4/27/201522017오류 유형: 280. HttpResponse.Headers.Add에서 "System.PlatformNotSupportedException: This operation requires IIS integrated pipeline mode." 예외 발생
... 121  122  123  124  [125]  126  127  128  129  130  131  132  133  134  135  ...