Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일
 

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://msdn.microsoft.com/en-us/library/system.web.configuration.httpruntimesection.requestvalidationmode(v=vs.110).aspx

이렇게 되면, 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> 내부의 데이터 조사에서 무한 루프가 발생할 수밖에 없도록 연결 리스트의 값이 설정되어 있는 것을 발견할 수 있었습니다.

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




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

[연관 글]





[최초 등록일: ]
[최종 수정일: 7/6/2018 ]

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

비밀번호

댓글 쓴 사람
 




... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
11471정성태3/20/20183110VC++: 125. CUDA로 작성한 RGB2RGBA 성능파일 다운로드1
11470정성태3/20/20185411오류 유형: 458. Visual Studio - CUDA 프로젝트 빌드 시 오류 C1189, expression must have a constant value
11469정성태3/19/20183005오류 유형: 457. error MSB3103: Invalid Resx file. Could not load file or assembly 'System.Windows.Forms, ...' or one of its dependencies.
11468정성태3/19/20183058오류 유형: 456. 닷넷 응용 프로그램 실행 시 0x80131401 예외 발생
11467정성태3/19/20182563오류 유형: 455. Visual Studio Installer - 업데이트 실패
11466정성태3/18/20183164개발 환경 구성: 355. 한 대의 PC에서 2개 이상의 DirectX 게임을 실행하는 방법
11463정성태3/15/20183473.NET Framework: 733. 스레드 간의 read/write 시에도 lock이 필요 없는 경우파일 다운로드1
11462정성태3/14/20183921개발 환경 구성: 354. HTTPS 호출에 대한 TLS 설정 확인하는 방법 [1]
11461정성태3/13/20185032오류 유형: 454. 윈도우 업데이트 설치 오류 - 0x800705b4 [1]
11460정성태3/14/20183410디버깅 기술: 112. windbg - 닷넷 메모리 덤프에서 전역 객체의 내용을 조사하는 방법
11459정성태3/13/20182557오류 유형: 453. Debug Diagnostic Tool에서 mscordacwks.dll을 찾지 못하는 문제
11458정성태2/21/20183079오류 유형: 452. This share requires the obsolete SMB1 protocol, which is unsafe and could expose your system to attack.
11457정성태2/17/20184782.NET Framework: 732. C# - Task.ContinueWith 설명파일 다운로드1
11456정성태4/10/20186373.NET Framework: 731. C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법 [3]파일 다운로드1
11455정성태2/17/20184323오류 유형: 451. ASP.NET Core - An error occurred during the compilation of a resource required to process this request.
11454정성태2/12/20188495기타: 71. 만료된 Office 제품 키를 변경하는 방법
11453정성태1/31/20182675오류 유형: 450. Azure Cloud Services(classic) 배포 시 "Certificate with thumbprint ... doesn't exist." 오류 발생
11452정성태1/31/20185601기타: 70. 재현 가능한 최소한의 예제 프로젝트란? [3]
11451정성태1/24/20183236디버깅 기술: 111. x86 메모리 덤프 분석 시 닷넷 메서드의 호출 인자 값 확인
11450정성태11/15/20189125Windows: 146. PowerShell로 원격 프로세스(EXE, BAT) 실행하는 방법
11449정성태3/6/20184588오류 유형: 449. 단위 테스트 - Could not load file or assembly 'Microsoft.VisualStudio.QualityTools.VideoRecorderEngine' or one of its dependencies. [1]
11448정성태1/20/20183138오류 유형: 448. Fakes를 포함한 단위 테스트 프로젝트를 빌드 시 CS0619 관련 오류 발생
11447정성태1/20/20183533.NET Framework: 730. dotnet user-secrets 명령어파일 다운로드1
11446정성태1/20/20183476.NET Framework: 729. windbg로 살펴보는 GC heap의 Segment 구조파일 다운로드1
11445정성태1/20/20183045.NET Framework: 728. windbg - 눈으로 확인하는 Workstation GC / Server GC
11444정성태9/13/20183822VS.NET IDE: 125. Visual Studio에서 Selenium WebDriver를 이용한 웹 브라우저 단위 테스트 구성파일 다운로드1
... [31]  32  33  34  35  36  37  38  39  40  41  42  43  44  45  ...