-
SNAPPY: Programmable Kernel-Level Policies for Containers논문정리 2021. 12. 13. 21:54
요약
컨테이너는 호스트와 전체 커널을 공유하기 때문에 호스트와 시스템의 다른 컨테이너를 손상시킬 수 있는 공격에 더 취약하다. 이 논문에서는 컨테이너와 같이 권한이 없는 프로세스도 eBPF 보안 정책을 런타임에 안전하고 동적으로 시행할 수 있는 새로운 framework인 SNAPPY를 제시한다. 이는 새로운 LSM (Linux 보안 모듈) 모듈, 새로운 보안 Linux 네임 스페이스 추상화 (policy_NS) 및 '동적 도우미'로 강화된 eBPF 정책으로 협력하여 수행된다. 이 디자인은 특히 컨테이너의 attack surface를 최소화할 수 있다. 우리의 디자인은 모든 프로세스에 적용될 수 있지만 특히 컨테이너 기반 사용 사례에 적합하다. 우리는 SNAPPY가 다양한 사용 사례에 대해 컨테이너의 보안 수준을 효과적으로 향상시킬 수 있고 가장 관련성이 높은 표준 (OCI, Open Container Initiative) 및 컨테이너화 엔진 (Docker 및 runC)과 쉽게 통합될 수 있으며 현실적인 시나리오에서 성능 오버 헤드가 0.09 % 미만임을 보여준다.
Introduction
컨테이너는 시스템 콜을 통해 호스트 커널과 상호 작용한다. 이 때 컨테이너와 호스트 간의 격리는 리눅스 네임 스페이스를 통해 이루어진다. 각 네임스페이스는 PID, 사용자 IPC 등의 리소스 관리를 담당한다. 이러한 메커니즘 덕분에 컨테이너가 일반적인 process보다 더 나은 격리 수준을 얻을 수 있다. 그러나 컨테이너는 VM과 달리 전체 linux 커널을 공유한다. 이는 컨테이너의 attack surface가 VM보다 더 넓다는 것을 의미한다. 따라서 컨테이너의 격리 및 보안 수준은 설계 상 VM보다 낮다.
이 논문에서는 컨테이너와 같이 권한이 없는 프로세스도 런타임에 네임 스페이스별로 사용자 정의할 수 있는 커널 풍부하고 세분화되고 프로그래밍 가능한 정책에서 안전하게 시행할 수 있도록 하는 새로운 프레임 워크 인 SNAPPY(Safe Namespaceable And Programmable PolicY)를 제시한다. 이를 통해 공격 표면을 최소화할 수 있다. 이를 위해 이 문서에서 policy_NS라고 부르는 새로운 보안 지향 Linux 네임 스페이스를 제안한다. 따라서 이러한 정책은 네임 스페이스(또는 나중에 설명할 하위 항목의 네임 스페이스 중 하나)의 모든 프로세스에 적용된다. 이러한 프로그래밍 가능한 정책은 eBPF로 작성된다. 또한 보안 속성을 유지하면서 일부 eBPF 한계를 극복하기 위해 'dynamic helper' 개념을 제안한다. 이러한 추상화는 새로운 LSM 모듈에서 상호적으로 작동한다. 우리의 디자인은 네임 스페이스를 통해 모든 프로세스에 적용될 수 있지만 특히 컨테이너에 적합하다. 또한 우리의 프레임 워크를 사용하면 런타임에 커널에서 세분화된 프로그래밍 가능 정책을 정의할 수 있으므로 소프트웨어의 소스 코드를 수정하거나 동작을 중지하지 않고도 런타임에 새로 발견된 취약점을 완화하는 데 사용할 수 있다.
이 논문의 주요 contribution은 다음과 같다.
- LSM hook에 대한 네임 스페이스 별 정책을 정의할 수 있는 네임스페이스(policy_NS)
- 권한이 없는 프로세스도 eBPF 기반 정책을 커널에 적용할 수 있도록 하는 커널 인터페이스 (policy_NS 네임 스페이스에 연결됨)
- Dynamic eBPF helper
- SNAPPY를 관련 컨테이너 엔진(runC 및 Docker) 및 표준(OCI 및 Dockerfile)에 통합
SNAPPY
Figure 1. Global design of SNAPPY 이 항목에서는 LSM hook을 활용하는 새로운 프레임 워크 인 SNAPPY를 제시하여 컨테이너와 같이 권한이 없는 프로세스도 런타임에 네임 스페이스별로 사용자 정의할 수 있는 커널 세분화, 스택 및 프로그래밍이 가능한 eBPF 정책 내에서 안전하게 자체 시행할 수 있도록 한다. 특히 SNAPPY는 새로운 Linux 네임 스페이스인 policy_NS, LSM 및 eBPF을 사용한다. 먼저 프레임 워크의 주요 구성 요소에 대한 개요를 설명한다. SNAPPY는 커널 수준에서 이 네임 스페이스의 프로세스 보안을 처리하는 데 필요한 모든 요소를 포함하는 새로운 Linux 네임 스페이스 policy_NS를 정의하므로 네임 스페이스 별 보안 정책을 정의할 수 있다. 이는 호스트 및 다른 컨테이너의 정책과 다른 자체적인 정책이 필요한 컨테이너에 특히 유용하다. 모든 프로세스는 해당 용도로 설계된 커널 인터페이스를 통해 커널에 eBPF 정책을 로드하여 특정 LSM 작업(파일 열기, 패킷 전송 등)을 보호하기 위해 네임 스페이스에 정책을 로드 할 수 있다. 그 다음 이 정책은 모니터링 된 LSM 작업과 관련된 정책 목록이 포함된 필드에서 이를 시행한 프로세스의 policy_NS에 연결된다. 로드 된 후에는 지정된 네임 스페이스 또는 하위 항목 중 하나의 프로세스에서 이 특정 LSM 작업을 수행할 때마다 정책이 적용된다. 권한이 없는 컨테이너가 풍부하고 세분화된 정책을 실행할 수 있도록 관리자가 eBPF 정책에서 사용할 수 있는 커널 eBPF dynamic helper를 생성하고 푸시 할 수 있는 방법을 제공한다. 이 프로세스는 간단한 형식 (커널 모듈)에서 수행할 수 있다. 로드 된 후에는 기존 helper가 호출되는 것과 유사한 방식으로 eBPF 코드에서 helper를 호출할 수 있습니다. Figure 1은 SNAPPY의 글로벌 디자인을 나타낸다. ‘open’과 같은 시스템 콜이 실행되면 file_open과 같은 LSM hook를 트리거 할 수 있다. LSM 작업이 SNAPPY에 의해 모니터링 되는 경우 SNAPPY 핸들러는 LSM 작업에 적용할 수 있는 호출자의 네임 스페이스 및 하위 항목에 저장된 모든 eBPF 정책을 실행한다. 이러한 정책은 커널 구조체에 액세스 할 수 있는 dynamic helper 집합에 액세스 할 수 있다.
Policy_NS Linux
Figure 2. Example of policy_NS policy_NS를 사용하면 컨테이너와 같은 시스템 프로세스의 하위 집합에만 정책을 적용할 수 있다. SNAPPY는 임시 구현으로 네임 스페이스를 간단하게 에뮬레이트하는 일부 기존 LSM 모듈과 달리, policy_NS는 커널에서 완전히 구현된 실제 Linux 네임 스페이스이므로 다른 Linux 네임 스페이스와 동일한 방식으로 처리할 수 있다. 프로세스의 보안을 처리하기 위해 각 policy_NS에는 각 LSM 작업에 적용할 자체 정책 목록이 포함되어 있다. 이러한 정책은 전체 시스템이 아닌 지정된 네임 스페이스(또는 하위 네임 스페이스 중 하나)의 프로세스에 대해서만 적용된다. 따라서 특정 필수 정책을 특정 프로세스에 적용하려면 새 policy_NS 네임 스페이스를 생성하는 것으로 충분하다. 이 새 네임 스페이스에 적용되는 정책은 시스템의 나머지 부분에 적용되지 않는다. 이러한 방식으로 우리의 설계를 통해 네임 스페이스 별 정책을 쉽게 적용할 수 있으며, 권한 상승을 방지하기 위해 프로세스는 네임 스페이스를 변경할 수 없다. 프로세스는 현재 자식 프로세스의 새 네임 스페이스에 가입하는 것만 이 허용된다. 마찬가지로 새 프로세스가 생성되면 현재 네임 스페이스 또는 현재 네임 스페이스의 자식인 새 네임 스페이스에 위치할 수 있다. 우리의 프레임 워크를 통해 관리자는 SNAPPY 정책을 수락하는 LSM hook를 결정할 수 있다. 이를 통해 단순히 모든 hook를 활성화하면 발생하는 성능 오버헤드 없이 모든 LSM hook에 대해 보편적으로 SNAPPY를 사용할 수 있다. SNAPPY 정책은 AND 연산으로 적용된다. 즉, 정책을 추가하면 허용된 동작 집합을 줄이는 것만 가능하다. 또한 권한 상승을 방지하기 위해 새로 생성된 네임 스페이스는 현재 네임 스페이스의 정책을 상속받는다. Figure 2는 policy_NS의 단순화한 예시를 나타낸다. 각 프로세스가 policy_NS에 대한 pointer를 가지고 있으며, 해당 policy_NS는 각 LSM hook에 대한 정책을 가지고 있고, 각 네임스페이스는 각종 상태와 parent field인 policy_NS를 가리키고 있다.
eBPF-based programmable security policies
해당 논문에서는 다음과 같은 이유로 eBPF를 사용하였다.
- Code-base policy는 사용자 정의 logic을 수행할 수 있고, 이는 설정 또는 rule 기반 정책과 같이 고정된 시스템 rule보다 유연하게 동작할 수 있음
- eBPF는 context switch 없이 kernel 내에서 정책을 적용시킬 수 있어, 성능 향상에 좋음
- eBPF는 리눅스 커뮤니티에서 잘 정립되어 상대적으로 안정적임
Figure 3. Loading of eBPF policies 기본적으로 우리 디자인의 attack surface는 Linux 5.7에서 15개의 helper를 허용하는 XDP와 같은 다른 유형의 attack surface보다 작다. 일단 컴파일 되면 SNAPPY의 eBPF 정책은 Figure 3과 같이 활성화된 LSM 후크에 대해 루트가 아닌 사용자도 시행할 수 있다. 이는 우리가 제안하는 특정 커널 인터페이스를 통해 컴파일 된 eBPF 정책을 푸시 함으로써 수행할 수 있습니다. 현재 구현에서는 /sys/kernel/security/snappy/policies/file_open 인터페이스를 통해 정책을 전송하여 LSM hook file_open에 정책을 적용하거나 OCI 런타임 사양과 같은 상위 수준 방법을 사용하여 정책을 푸시 할 수도 있다. 그런 다음 eBPF verifier가 프로그램의 유효성을 검사한다. 프로그램이 유효하면 현재 프로세스의 policy_NS의 오른쪽 hook에 저장되고 이 hook에 대한 다른 정책이 있는 경우 이와 함께 기록되며, 이 정책은 네임 스페이스에 저장되는 즉시 적용된다. 따라서 이 네임 스페이스 (또는 하위 네임 스페이스 중 하나)의 프로세스가 이 정책에 의해 모니터링 되는 LSM hook 작업을 실행하려고 시도 할 때마다 이 정책은 다음에 이 네임 스페이스 또는 이 네임 스페이스의 부모 네임스페이스에 저장된 다른 정책과 함께 이 액세스를 확인하기 위해 실행된다. 우리는 Linux 보안 커뮤니티의 일부가 eBPF가 아직 충분히 안전하지 않을 수 있으므로 권한이 없는 사용자가 사용할 수 있는 새로운 eBPF 유형이 커널 메인 라인에 통합되는 것을 반대한다고 생각한다. 그러나 eBPF 정책은 컨텍스트 포인터에 대한 액세스를 거부하고 single helper 만 포함하는 새로운 eBPF 유형을 사용하기 때문에 공격 표면이 XDP와 같이 이미 통합된 사용 사례보다 낮다고 주장한다. 따라서 SNAPPY에서 취약점이 발견되면 이는 XDP에 이미 존재할 것이다.
eBPF dynamic helpers
Figure 4. Loading of dynamic helpers 이전 두 항목에서 사용된 기능을 통해 특정 동작 및 네임스페이스에 대한 정책을 실행할 수 있다. 그렇지만, eBPF type은 매우 제한적이므로 이러한 기능만으로는 단순히 동작 활성화 또는 거부이외의 세분화된 정책을 허용하지 않는다. 이 문제를 해결하기 위한 일반적인 접근 방식은 해당 eBPF type에 대한 helper를 구현하는 것이다. 이러한 helper는 커널에 네이티브 코드로 작성되고 커널과 함께 컴파일 된다. 이후 eBPF 코드를 대신하여 권한 접근 및 복잡한 작업을 담당하는 여타 기능처럼 eBPF 프로그램에서 호출할 수 있다. 그러나 우리의 프레임 워크는 가능한 한 일반적인 것을 목표로 하므로 커널에 매우 많은 수의 helper를 구현해야 하는데, 이는 attack surface를 크게 증가시키며, 일부 사용 사례와 관련된 모든 기능을 제공하는 것은 어렵다. 또한 helper를 추가하려면 커널을 다시 컴파일하고 시스템을 재부팅 해야 하므로 일부 사용 사례에서 문제가 될 수 있다. 따라서 정적 인 일반 eBPF helper와 대조되는 'dynamic helper'개념을 제안한다(예: 커널에 통합되어 있으며 커널을 수정하고 다시 컴파일하지 않고는 helper를 추가할 수 없음). Dynamic helper의 생성과 로딩은 Figure 4에 설명되어 있다. helper는 관리자가 커널 모듈이라는 비교적 간단한 형식으로 작성할 수 있다. 그러한 커널 모듈은 하나 또는 여러 개의 helper를 포함할 수 있으며, 각각은 서로 다른 기능을 수행한다. 우리는 이 helper 세트를 라이브러리라고 지칭한다.
Figure 5. Calling a dynamic helper from eBPF code 라이브러리는 커널 심볼에서 테라 바이트 떨어진 주소 공간에 __vmalloc을 사용하여 할당된다. 그러나 x86-64 커널은 현재 명령어의 2GB 미만에서만 기호를 참조할 수 있는 작은 메모리 모델을 사용하기 때문에 gcc로 간접 호출을 사용하도록 강제한다. 이러한 간접 호출은 64 비트 절대 주소를 사용하여 심볼을 호출할 수 있으므로 이러한 함정을 피할 수 있다. 외부 심볼의 주소는 System.map을 사용하여 검색됩니다. KASLR (Kernel Address Space Layout Randomization)은 로드 시 helper의 GoT(Global Offset Table)의 각 항목에 적절한 커널 오프셋을 추가하기만 하면 처리된다. 이러한 helper는 일부 메타 데이터(이 라이브러리를 적용할 수 있는 LSM hook, 라이브러리에 대한 helper 오프셋 등)가 있는 커널 인터페이스 (/sys/kernel/security/snappy/snappy_load)를 통해 커널로 푸시된다. 로드 되면 이러한 helper는 eBPF 코드에서 호출할 준비가 된다. 이전 섹션에서 설명했듯이 새로운 eBPF type에는 snappy_dynamic_call(int lib_id, int fn_id, void ** args) 프로토타입이 있는 정적 도우미가 하나만 있다. 이 정적 함수는 그림 5와 같이 dynamic helper를 호출하는 프록시 역할을 한다. 따라서 (라이브러리 ID, 도우미 ID) 튜플을 사용하여 dynamic helper를 호출할 수 있다. 이 튜플은 우리가 정의한 특정 커널 인터페이스를 사용하여 찾을 수 있으며 모든 사용자가 읽을 수 있다. eBPF 컨텍스트를 포함하여 모든 인수를 이 dynamic helper에 void **로 전달할 수 있다. eBPF 프로그램이 동적 도우미를 호출하려고 할 때 호출된 라이브러리와 helper가 있는지 확인합니다. 존재하는 경우 helper (라이브러리의 기본 주소 + 이 helper의 오프셋)를 참조하여 함수 포인터가 생성된다. 도우미는 마지막으로 주어진 인수로 실행되고 이 helper의 결과는 eBPF 코드로 반환된다. 결론적으로, 우리의 디자인은 우리의 주요 목표였던 컨테이너와 같은 네임 스페이스의 프로세스에 대해 런타임에 스택 가능하고 세분화되고 프로그래밍 가능한 정책을 효과적으로 시행할 수 있도록 한다.
Security analysis of SNAPPY
- SNAPPY를 사용하면 권한이없는 사용자도 자신의 policy_NS 네임 스페이스에 연결할 커널에 eBPF 정책을 푸시 할 수 있다. eBPF 안전 특성으로 인해 정책은 시스템에 해를 끼칠 수 없다. 또한 프로세스는 네임 스페이스 외부에 적용될 정책을 생성할 수 없으므로 나머지 시스템을 방해할 수 없다. 프로세스가 수행할 수 있는 유일한 일은 예상되는 동작을 차단할 수 있는 "잘못된"정책을 자신에게 적용하는 것이다. 이는 DoS에서 최악의 시나리오를 초래할 수 있다. 프로세스 또는 컨테이너가 예를 들어 pkill 또는 rm -rf를 사용하여 이미 자체 DoS를 수행할 수 있기 때문에 이것은 보안 문제가 아니다.
- 전체 네임 스페이스 수명주기 동안 eBPF 정책을 삭제할 수 없기 때문에 프로세스는 SNAPPY를 사용하여 새로운 권한을 얻을 수 없으므로 권한 상승이 방지된다.
- SNAPPY를 사용하면 관리자가 사용자가 풍부하고 세분화된 eBPF 정책을 쉽게 정의하는 데 사용할 수 있는 커널 eBPF helper를 로드 할 수 있다. 관리자가 이 기능을 악의적으로 사용하면 서비스 거부 또는 커널 패닉과 같은 심각한 문제가 발생할 수 있지만 이러한 동작은 SNAPPY없이도 악의적 인 관리자가 시스템을 삭제하거나 악성 커널 모듈을 삽입하여 수행될 수 있다.
- SNAPPY는 시스템의 자원인 SNAPPY를 과도하게 사용하고 DoS를 생성하여 프로세스가 고갈되는 것을 방지하기 위해 제한을 적용한다. 따라서 경험적으로 네임 스페이스의 개수를 최대 32 개 수준으로 고정하고 시스템에 추가할 수 있는 eBPF 정책 및 helper 수에 대한 제한도 설정했다. 따라서 SNAPPY의 어떤 기능도 공격이나 DoS를 생성하는 데 직접 사용할 수 없다.
Related ART
Table 1. Comparison between SNAPPY and the related art Landlock LSM은 컨테이너와 같은 프로세스가 커널 수준에서 스스로를 샌드박스로 만들어 자신의 권한을 제한하여 잠재적인 손상을 완화할 수 있도록 하는 프레임 워크이다. SNAPPY와 마찬가지로 Landlock은 eBPF 정책이 모든 프로세스에서 커널로 푸시되도록 허용하여 이를 구현하였다. Landlock은 새로운 전용 시스템 콜(landlock)에 의존한다. 마찬가지로 SNAPPY의 경우 이러한 정책은 필수이고 스택 가능하며 시스템의 하위 집합에만 적용할 수 있다. 예를 들어 MAC 정책을 적용하여 읽기-쓰기 파일 및 읽기 전용 파일에 대한 화이트리스트를 정의할 수 있습니다. 다른 모든 액세스는 이러한 정책에 의해 거부된다. 그러나 SNAPPY와 달리 Landlock의 디자인은 일반적이지 않으며 파일, 자격 증명 및 추적과 관련된 몇 가지 hook만 처리한다. 또한 Landlock LSM에는 네임 스페이스 개념이 없다. 대신 프로세스와 그 자손에 정책이 적용된다. 따라서 Landlock LSM은 상위 링크가 없는 프로세스에 동일한 정책을 적용할 수 없다. 시스템 데몬을 통해 생성된 컨테이너는 이를 생성한 프로세스의 정책을 상속하지 않으므로 권한 상승 위험이 있다. 두 프레임 워크 모두 네임 스페이스 정책을 실행할 수 있지만 SNAPPY는 유효한 eBPF 코드를 실행하고 동적 도우미 호출을 사용할 수 있기 때문에 보안 네임 스페이스에서 수행되는 것처럼 시스템에서 단일 LSM을 적용하는 것보다 훨씬 일반적이며, SNAPPY는 수정되지 않은 확립된 요소들(eBPF, 커널 객체, LSM, ...)을 기반으로 하기 때문에 복잡하고 버그가 발생하기 쉬운 작업 인 LSM 모듈을 수동으로 조정하는 것보다 구현의 정확성을 확인하기가 더 쉽다. 또한 SNAPPY의 성능 오버 헤드는 현재 활성화된 policy_NS와 그 조상(일반적으로 3 개 미만)을 확인하기만 하면 되며, 보안 네임 스페이스와 같이 큰 서버에서 엄청난 수인 모든 네임 스페이스를 확인해야하기 때문에 보안 네임 스페이스보다 더 잘 확장할 수 있다. 이러한 이유로 우리는 우리의 디자인이 보안 네임 스페이스보다 권한이 없고 신뢰할 수 없는 컨테이너 보호에 더 적합하다고 생각한다. 다른 접근 방식은 eBPF를 활용하여 시스템 수준에서 보안을 개선한다. 그러나 이러한 접근 방식 중 어느 것도 이 논문에서 앞서 언급 한 과제를 해결할 수 없다.
BPFBox는 커널에 eBPF 정책을 적용하여 프로세스의 격리를 향상시킬 수 있다. 그러나 BPFBox는 컨테이너에 중점을 두지 않으며 해당 정책은 관리자가 시행해야 한다.
Cilium은 eBPF를 활용하여 컨테이너 통신을 보호한다. 예를 들어 API에서 특정 명령 만 호출하도록 허용하기 위해 계층 7 방화벽으로 사용할 수 있다. 그러나 이는 네트워크에 집중되어 컨테이너 자체를 보호하는 데 사용할 수 없다.
KRSI는 관리자가 eBPF 정책을 LSM hook에 적용하여 잠재적 공격의 증상을 감지하는 데 사용할 수 있다(예: 프로세스가 LD_PRELOAD를 사용함). 그러나 이러한 정책은 단일 컨테이너가 아닌 전체 시스템에 적용되어야 하며 KRSI는 권한이 없는 주체가 사용할 수 없다.
Falco [32]는 위협 탐지를 목표로 하는 시스템 콜 추적 프로그램이다. 커널 모듈 또는 eBPF에 의존할 수 있다. 그러나 Falco는 네임 스페이스 메커니즘을 제공하지 않으므로 네임 스페이스 별 정책을 허용하지 않는다. 또한 시스템 콜 인터셉션 기반 접근 방식은 커널 버전에 매우 의존적이며 보안 동작을 매핑하는 통합된 방법을 제공하지 않으므로 LSM 기반 접근 방식보다 신뢰성이 떨어진다.
Seccomp-BPF [10]은 BPF(classic bpf)를 사용하여 시스템 콜 필터링을 수행한다. 프로세스의 attack surface를 제한할 수 있지만 커널 포인터를 역참조 할 수 없어 상세한 제한이 불가능하다. 표 2에서는 SNAPPY와 유사한 관련 기술 간의 기능 비교를 정리한다.
결론
Limitations
- 관리자를 신뢰하기 때문에 helper의 일관성이 확인되지 않는다(예: 보호 장치 없음).
- helper는 커널에서 재귀를 피하기 위해 호출 한 hook를 호출할 수 없다. 다행스럽게도 helper가 적절하게 사용된다면 (즉, 내부 커널 함수에만 의존하고 유저 스페이스 wrapper를 사용하지 않는 경우) 이런 일이 발생하지 않는다. 현재 SNAPPY hook에 있을 때 LSM hook의 트리거를 차단하여 이 문제를 해결할 수 있는지 조사하고 있다.
Future work
- 실제 컨테이너를 보호하는 정책을 개발
- 기존 LSM 모듈을 SNAPPY 라이브러리로 변환하여 권한이 없는 컨테이너가 나머지 시스템과 독립적으로 투명하게 사용할 수 있도록 개선. 그러면 LSM 네임스페이스를 SNAPPY 안으로 옮겨 현재 LSM 스택 및 네임 스페이스의 문제를 방지할 수 있다.
Reference
[1] Belair, Maxime, Sylvie Laniepce, and Jean-Marc Menaud. "SNAPPY: programmable kernel-level policies for containers." Proceedings of the 36th Annual ACM Symposium on Applied Computing. 2021.
'논문정리' 카테고리의 다른 글
MongoDB performance bug 정리 (0) 2022.02.09 Root Cause Analysis 관련 논문 조사 (0) 2021.12.13 Detecting Motifs in System Call Sequences (0) 2020.04.22 Hierarchical Pattern Discovery in Graphs (0) 2020.03.29 DualIso: An Algorithm for Subgraph Pattern Matching on Very Large Labeled Graphs (0) 2020.03.24