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

비밀번호

댓글 작성자
 




... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11657정성태8/18/201819916사물인터넷: 30. 릴레이(Relay) 제어파일 다운로드3
11656정성태8/16/201815689사물인터넷: 29. 트랜지스터와 병렬로 연결한 LED파일 다운로드1
11655정성태8/16/201817950사물인터넷: 28. 저항과 병렬로 연결한 LED파일 다운로드1
11654정성태8/15/201819209사물인터넷: 27. 병렬 회로의 저항, 전압 및 전류파일 다운로드1
11653정성태8/14/201820047사물인터넷: 26. 입력 전압에 따른 LED의 전압/저항 변화 [1]파일 다운로드1
11652정성태8/14/201817474사물인터넷: 25. 컬렉터 9V, 베이스에 5V와 3.3V 전압으로 테스트하는 C1815 트랜지스터파일 다운로드1
11651정성태8/14/201822610사물인터넷: 24. 9V 전압에서 테스트하는 C1815 트랜지스터 [1]파일 다운로드3
11650정성태8/14/201817008사물인터넷: 23. 가변저항으로 분압파일 다운로드1
11649정성태8/12/201819332사물인터넷: 22. 저항에 따른 전류 테스트파일 다운로드1
11648정성태8/12/201820757사물인터넷: 21. 퓨즈를 이용한 회로 보호파일 다운로드3
11647정성태8/8/201820891오류 유형: 476. 음수의 음수는 여전히 음수가 되는 수(절대값이 음수인 수)
11646정성태8/8/201816921오류 유형: 475. gacutil.exe 실행 시 "Failure initializing gacutil" 오류 발생
11645정성태8/8/201819128오류 유형: 474. 닷넷 COM+ - Failed to load the runtime. [1]
11644정성태8/6/201822031디버깅 기술: 118. windbg - 닷넷 개발자를 위한 MEX Debugging Extension 소개
11643정성태8/6/201821642사물인터넷: 20. 아두이노 레오나르도 R3 호환 보드의 3.3v 핀의 LED 전압/전류 테스트 [1]파일 다운로드1
11642정성태8/3/201820463Graphics: 20. Unity - LightMode의 ForwardBase에 따른 _WorldSpaceLightPos0 값 변화
11641정성태8/3/201825985Graphics: 19. Unity로 실습하는 Shader (10) - 빌보드 구현 [1]파일 다운로드1
11640정성태8/3/201822155Graphics: 18. Unity - World matrix(unity_ObjectToWorld)로부터 Position, Rotation, Scale 값을 복원하는 방법파일 다운로드1
11639정성태8/2/201819732디버깅 기술: 117. windbg - 덤프 파일로부터 추출한 DLL을 참조하는 방법
11638정성태8/2/201818124오류 유형: 473. windbg - 덤프 파일로부터 추출한 DLL 참조 시 "Resolved file has a bad image, no metadata, or is otherwise inaccessible." 빌드 오류
11637정성태8/1/201822524Graphics: 17. Unity - World matrix(unity_ObjectToWorld)로부터 TRS(이동/회전/크기) 행렬로 복원하는 방법파일 다운로드1
11636정성태8/1/201829925Graphics: 16. 3D 공간에서 두 점이 이루는 각도 구하기파일 다운로드1
11635정성태8/1/201818641오류 유형: 472. C# 컴파일 오류 - Your project is not referencing the ".NETFramework,Version=v3.5" framework.
11634정성태8/1/201821595.NET Framework: 790. .NET Thread 상태가 Cooperative일 때 GC hang 현상 재현 방법파일 다운로드1
11633정성태7/29/201825521Graphics: 15. Unity - shader의 World matrix(unity_ObjectToWorld)를 수작업으로 구성 [2]파일 다운로드1
11632정성태7/28/201827839Graphics: 14. C# - Unity에서 캐릭터가 바라보는 방향을 기준으로 카메라의 위치 이동 및 회전하는 방법
... [91]  92  93  94  95  96  97  98  99  100  101  102  103  104  105  ...