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

비밀번호

댓글 작성자
 




... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11258정성태8/1/201720329.NET Framework: 668. 지연 서명된 DLL과 서명된 DLL의 차이점파일 다운로드1
11257정성태7/31/201719928.NET Framework: 667. bypassTrustedAppStrongNames 옵션 설명파일 다운로드1
11256정성태7/25/201721849디버깅 기술: 90. windbg의 lm 명령으로 보이지 않는 .NET 4.0 ClassLibrary를 명시적으로 로드하는 방법 [1]
11255정성태7/18/201724397디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201720804개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201721442Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201719188오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201718642디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201722282디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201720021오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
11248정성태7/12/201726556오류 유형: 409. pip install pefile - 'cp949' codec can't decode byte 0xe2 in position 208687: illegal multibyte sequence
11247정성태7/12/201720920오류 유형: 408. SqlConnection 객체 생성 시 무한 대기 문제파일 다운로드1
11246정성태7/11/201718903VS.NET IDE: 118. Visual Studio - 다중 폴더에 포함된 파일들에 대한 "Copy to Output Directory"를 한 번에 설정하는 방법
11245정성태7/10/201724640개발 환경 구성: 321. Visual Studio Emulator for Android 소개 [2]
11244정성태7/10/201724832오류 유형: 407. Visual Studio에서 ASP.NET Core 실행할 때 dotnet.exe 프로세스의 -532462766 오류 발생 [1]
11243정성태7/10/201721604.NET Framework: 666. dotnet.exe - 윈도우 운영체제에서의 .NET Core 버전 찾기 규칙
11242정성태7/8/201721161제니퍼 .NET: 27. 제니퍼 닷넷 적용 사례 (7) - 노후된 스토리지 장비로 인한 웹 서비스 Hang (멈춤) 현상
11241정성태7/8/201719793오류 유형: 406. Xamarin 빌드 에러 XA5209, APT0000
11240정성태7/7/201723630.NET Framework: 665. ClickOnce를 웹 브라우저를 이용하지 않고 쿼리 문자열을 전달하면서 실행하는 방법 [3]파일 다운로드1
11239정성태7/6/201724298.NET Framework: 664. Protocol Handler - 웹 브라우저에서 데스크톱 응용 프로그램을 실행하는 방법 [5]파일 다운로드1
11238정성태7/6/201721802오류 유형: 405. NT 서비스 시작 시 "Error 1067: The process terminated unexpectedly." 오류 발생 [2]
11237정성태7/5/201723460.NET Framework: 663. C# - PDB 파일 경로를 PE 파일로부터 얻는 방법파일 다운로드1
11236정성태7/4/201727214.NET Framework: 662. C# - VHD/VHDX 가상 디스크를 마운트하지 않고 파일을 복사하는 방법파일 다운로드1
11235정성태6/29/201721496Math: 20. Matlab/Octave로 Gram-Schmidt 정규 직교 집합 구하는 방법
11234정성태6/29/201719015오류 유형: 404. SharePoint 2013 설치 과정에서 "The username is invalid The account must be a valid domain account" 오류 발생
11233정성태6/28/201718837오류 유형: 403. SharePoint Server 2013을 Windows Server 2016에 설치할 때 .NET 4.5 설치 오류 발생
... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...