macOS Catalina 버전부터 JDK의 System.loadLibrary 가 에러가 날 때

정말 오래간만에 자바 관련한 블로깅입니다.


불편한 사람들이 있을 것 같아서 정보 공유 차원에서 현재까지 파악한 이슈를 남깁니다.

macOS Catalina(10.15.3) 버전부터 JDK의 System.loadLibrary를 통한 dylib 파일 로드가 실패합니다.

이와 관련해서는 크게 두 가지 문제가 관련되어 있습니다.


1. System.loadLibrary 버그

공식 JDK 관련 이슈는 다음 이슈에서 볼 수 있습니다.

Java Bug System : System.loadLibrary fails on Big Sur for libraries hidden from filesystem

GitHub : System.loadLibrary fails on Big Sur for libraries hidden from filesystem

System.loadLibrary()를 통한 경로 찾기가 실패하는가 하고 여러 시도를 해보면 곧 System.load()를 통해 dylib 파일을 직접 읽어도 제대로 동작하지 않는다는 것을 알게 될 것입니다.

JDK 라이브러리 로딩 버그의 원인(아직 미해결)을 옮겨봅니다.

System.loadLibrary 실패 원인

문제 설명: 

OSX Big Sur no longer ships with copies of the libraries on the filesystem and therefore attempts to load a native library via System.loadLibrary no longer works.
애플 페이지 내용: New in macOS Big Sur 11.0.1, the system ships with a built-in dynamic linker cache of all system-provided libraries. As part of this change, copies of dynamic libraries are no longer present on the filesystem. Code that attempts to check for dynamic library presence by looking for a file at a path or enumerating a directory will fail. Instead, check for library presence by attempting to dlopen() the path, which will correctly check for the library in the cache. (62986286)
Big Sur부터는 동적 라이브러리의 사본들을 파일 시스템을 통해 찾을 수 없다. 따라서 해당 경로에 파일이 있는지를 체크하거나 해당 디렉토리에 있는 파일들을 찾아보도록 구현된 코드들은 실패하게 된다. macOS에서는 그냥 dlopen을 해당 경로로 주면 알아서 캐시에 있는 라이브러리를 체크할 수 있다. 즉, JDK에서 불필요하게 파일의 존재 유무를 체크하지 않고 바로 dlopen만으로 체크해야 한다.


상세 설명: 

- The new dynamic linker cache introduced by OSX Big Sur creates problems with previous code which checks for the existence of a library file in the filesystem before attempting to load it with dlopen(...). According to the Big Sur release notes in [1],
"Code that attempts to check for dynamic library presence by looking for a file at a path or enumerating a directory will fail. Instead, check for library presence by attempting to dlopen() the path, which will correctly check for the library in the cache."

Tracing the execution of System.loadLibrary shows that the offending check happens at [2] (tested on OpenJDK 11 and 17, Oracle JDK 17 and 18-ea) where File.exists(...) is used to check whether the library file is present on the filesystem before attempting to load it. As a result, valid dynamic libraries that otherwise open fine via dlopen(...) fail with UnsatisfiedLinkError in current versions of the JDK.
현재 JDK 구현체들은 로드하기 위하여 파일 시스템의 파일 존재 유무를 File.exists(...) 코드를 사용하여 체크하도록 되어 있다. 그래서 그냥 dlopen(...)으로 바로 열면 제대로 열리는 유효한 동적 라이브러리들이 현재 JDK 버전들에서는 UnsatisfiedLinkError를 던진다.

즉,  DYLD_LIBRARY_PATH를 지정하여 해당 디렉토리를 뒤져 경로 방식으로  로드하는 건  무조건 실패하게 됩니다.

2. macOS의 라이브러리 공증 notarization 요구 사항

System.loadLibrary 뿐만 아니라 macOS의 라이브러리 관련 보안 공증 요건 강화도 영향을 주고 있습니다.

이런 에러가 특정 macOS 버전부터 발생하는 이유는 macOS의 라이브러리 보안 관련 규정들이 바뀌었기 때문인데...


이에 대한 내용은 다음에서 잘 정리해두었습니다.


내용을 간단히 살펴보면 이와 같은 오류가 발생하는 원인은 다음과 같습니다.

에러의 원인

Apple now requires Application executables like Java to be Notarized. As part of this process, with MacOS 10.15.3, the executables must be Hardened Runtime. In order to be Notarized, and once the Hardened Runtime executables and libraries are installed in the /System and /Library hierarchies, they cannot load libraries which have not been Notarized. Note that Apple has been tightening this up progressively.
애플은 macOS 특정 버전부터 응용 프로그램 바이너리들에 대해 공증을 요구하고 있는데, macOS 10.15.3부터는 이 공증 바이너리의 절차로 실행 바이너리에 보안 강화(hardening) 과정을 거치도록 하고 있다. 공증되기 위해서는 일단 /System 과 /Library 폴더 아래에 한번 설치되는 강화 런타임 실행 파일과 라이브러리들은 공증되지 않는 라이브러리를 적재할 수가 없다. 애플은 이 요구를 점차 더 강화하고 있다.

해결방안

In the short term, the resolution is to download the zip image of the version of Azul Zulu Builds of OpenJDK that you need, and extract the bundle to a folder outside /Library and /System. When you want to run your application, first set DYLD_LIBRARY_PATH=/usr/local/lib, and then use the java executable from the unzipped install. In time, as Apple extends its security model, this option may go away.
In order to ensure that your application is able to continue to load any third-party libraries in the future, contact the library authors and let them know that you need them to submit their libraries for Notarization.
단기적인 해결책으로는 필요한 JDK를 /System이나 /Library 폴더의 자식 경로가 아닌 경로에 설치하면 이 강화 과정을 회피할 수 있다. 그리고 원하는 dylib 라이브러리도 다른 경로에 두고 환경변수 DYLD_LIBRARY_PATH로 경로를 설정해주면 해당하는 문제를 피할 수 있다. 하지만 애플이 보안 모델을 더 강화하면 앞으로는 안될 수도 있다.
가장 확실한 방법은 dylib 라이브러리들을 공증받도록 유도하는 게 최선이다.

일단 JDK 설치를 기본 경로인 /Library 아래에 해서는 답이 없다는 것을 알 수 있습니다.


3. 당장의 Workaround 방법

실제 테스트를 zulu openjdk 8과 11 버전을 통해서 해본 바는 다음과 같습니다.

먼저 JDK 설치 경로는 기본이 /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home 과 같은 곳에 설치되는데 앞에서 언급한 /Library 서브폴더의 요건을 회피하기 위해 사용자 디렉토리로 통째로 옮깁니다.

~/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home

일단 이렇게 하고 난 다음에도 DYLD_LIBRARY_PATH로 설정된 경로에 있는 dylib 동적 라이브러리들을 찾지 못하는 문제가 있습니다. 이 이유는 앞에서 언급한 JDK 라이브러리 로딩 버그와 상관이 있을 것입니다.

JDK가 별도의 loadLibrary 호출 없이 기본으로 찾을 수 있는 다른 가능한 경로에 dylib을 복사하여 실행가능하면 개인적으로는 문제가 해결되기 때문에 (개발과 테스트 목적이므로) 다음과 같이 해결하였습니다. 

JDK 8의 경우에는 아래 위치에 원하는 dylib 파일을 복사

~/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/server

JDK 11의 경우에는 아래 위치에

~/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/lib/server


#또하나의삽질

댓글

이 블로그의 인기 게시물

[Java] Java G1 GC의 특성에 따른 Full GC 회피 튜닝 방법

일론 머스크의 First Principle Thinking (제1원리 기반 사고)

엄밀한 사고(Critical Thinking)란 무엇일까