學術乾貨|深入淺出解讀 Java 虛擬機器的差別測試技術
本文分享基於位元組碼種子生成有效、可執行的位元組碼檔案變種,並用於 JVM 實現的差別測試。本文特別提出用於修改位元組碼語法的classfuzz技術和修改位元組碼語義的classming技術。上述變種技術系統性地操作和改變位元組碼的語法、控制流和資料流,生成具有豐富語義的位元組碼變種。
進一步地,可以在多個 JVM 產品上執行生成的位元組碼變種,透過 JVM 驗證或執行行為的差異以發現 JVM 缺陷乃至安全漏洞。本文整理自陳雨亭在 2018 年 12 月 22 日 GreenTea JUG Java Meetup 現場的演講速記。
陳雨亭
上海交大博士,副教授
近年來研究將模糊測試技術應用於高複雜軟體系統的測試用例的自動生成和系統分析中。其設計開發了 Java 位元組碼自動生成工具,可以自動生成 Java 位元組碼以發現 Java 虛擬機器規範與實現中缺陷,保障虛擬機器的可靠性、安全性。研究成果也發表於 PLDI、FSE、ASE 等軟體領域重要的國際會議上。
今天我要報告的是我們在過去幾年內針對 Java 虛擬機器的測試工作。首先先做一下自我介紹,我是中國計算機學會系統軟體專委會委員陳雨亭,非常希望有同仁加入系統軟體專委會。
對於 Java 虛擬機器測試的研究,其實是一個偶然。早期,我做了一些軟體測試方面的工作,當時我更多關注於技術,包括基於規約的軟體測試、模型驅動的軟體測試、白盒測試、黑盒測試這些耳熟能詳的測試技術。
在 2014 年到 2015 年之間,我開始關注那些能夠發現真實問題的系統測試方面的工作,當時就做了 SSL 安全協議支撐軟體,包括 OPENSSL 這樣的一些軟體的測試。後來就想為什麼不能做一些更復雜工作?比如可以測試 Java 虛擬機器,隨後就遇上了一個新的挑戰, Java 虛擬機器的輸入是位元組碼,對其測試某種意義上來說實際上是在生成程式,這件事情也很有挑戰。
我們在 JVM 測試方面做了兩項工作,實際上做了兩個工具:一個是 Classfuzz;一個是 Classming。
首先介紹一下背景,這個問題的背景還是來自於 Java 虛擬機器的跨平臺性,對於同樣的類來說,放在各個虛擬機器上面跑,就需要有相同的執行結果。對於 Java 虛擬機器,我們就想能不能在裡面找到一些缺陷。
實際上這個不是一個新概念,任何一個產品級的虛擬機器在釋出之前都需要透過技術相容包 TCK 的測試,那麼技術相容包實際上是由 Oracle 釋出的。這就引發了新問題,我不是 Oracle 的員工,我也沒有花錢去買 TCK,我該怎樣去測試一個已經發布了的產品級虛擬機器?包括 OpenJDK 中的 HotSpot 或者 IBM 的 OpenJ9。
這裡面就同時衍生了兩個問題:
怎麼樣去發現一個 JVM 缺陷或者是安全漏洞?
怎樣生成一個有效的測試包?對於測試輸入,怎樣能夠有更多的這樣的位元組碼,或者產生更多可執行的應用程式並在虛擬機器上再跑一跑?
如何暴露出產品級虛擬機器的缺陷
對於第一個問題,即怎麼樣去暴露出一個產品及虛擬機器的缺陷,這裡面在跑的時候就會發現有一個困難,這個困難就是在學術圈裡叫做“缺少一個測試喻言”。
如果要測一個 Java 虛擬機器的話,我們拿一個類過來跑跑,在一個 Java 虛擬機器上面,會得到一個真實的結果,這個時候我們把真實結果和一個預期結果來比較一下,如果能夠發現它們裡面的不一致,那麼這個就說明 Java 虛擬機器出現了一些問題。
我們的預期結果到底是怎麼來的?實際上有一個 Java 虛擬機器規範,假如 Java 虛擬機器規範,它也是一個能夠執行的機器,那麼它跑一跑,能夠得到一個預期結果。但是實際上,我們說 Java 虛擬機器規範它本身也不能跑,所以這個事情實際上沒有很好的解決方案。
後來,我們意識到差別測試技術,這個也是 Java 虛擬機器開發中,大家都採用的一個方法,也就是說有多個虛擬機器,把一個類或者是應用拿到不同的虛擬機器上去跑,比較它們之間的結果是不是有差別。
如果這個大家結果都一致,那就很好,如果結果不一致,那麼就可以去預測一下這裡面是不是有 Java 虛擬機器的實現出了問題。
如何獲得一個有效的測試包
對於第二個問題,就是怎麼樣能夠有更加複雜的或者更加花樣繁多的位元組碼來做測試?一開始,我們嘗試去使用現實中大量的類,從網上甚至從 openJDK 裡面自己的包裡解壓出很多類檔案,放在 JVM 不同版本上面去跑。這裡面的確還是能夠發現一些問題,一些不一致的現象,但是這個不一致更多是相容性問題。
於是我們很快就轉向了第二個技術,叫“領域感知的模糊測試技術”。模糊測試是應用在安全領域裡面的一個測試技術,它可以幫助發現一些安全問題。比如說有一個檔案,有個影像,把這個影像一位一位地變化,用以檢視應用軟體是否比較健壯。
如果把技術應用到 Java 虛擬機器上面,就要做一些調整,這種調整是領域感知的 ,也就是說我們知道 Java 位元組碼它本身的一些特性,根據它的特性來做一些變化,這個工作更加泛,我們有一個種子類,透過這個種子,我們會把它變來變去,變成一堆的測試類,放到 Java 虛擬機器上跑。
這個工作我們曾發在 PLDI2016,還有一個明年的 ICSE 上的工作,第一個是 classfuzz,第二個 classming。讓我們對於 Java 的類執行過程進行一個深入瞭解,一開始做的工作比較偏向於上層,就是更多的去關注了 Java 類的是怎麼樣去導進來,怎麼樣連結,怎麼樣去初始化等等。
這個是 classfuzz 的主要工作。後來做到一定的程度,我們就轉向了下層,怎麼樣去做驗證,執行,這個時候就會去想類的執行會不會引發一些差別,我能不能在不同虛擬機器上真的跑出一些不一樣的結果?
Classfuzz
下面,我就分別對這兩個工作進行介紹。classfuzz 是一個很簡單的一個想法,就有點像一開始最傳統的模糊測試的技術,對合法的 Java 位元組碼檔案,我們想進行一個語法變種,變種以後,比如說對於 Java 類我們得到它的一個語法樹,去嘗試修改,比如說把 public 改成 private,把檔名改一改,把這個函式名改一改,這樣的話可以生成很多比較奇怪的類,把奇怪的類拿過來以後,就可以去測試一下 Java 虛擬機器的一個健壯性。
但是這個時候我們很快就意識到,這個時候它有一個缺陷,我們只是去挑戰了虛擬機器的一個啟動過程,就是看看它的格式檢查對不對?去看看他的連結過程對不對,看看它的初始化對不對。Classfuzz 是 2016 年的一個論文,但一直到近期我們還是用它發現了一點問題。
右邊是一個位元組碼,當然這個位元組碼比較繁雜,把這樣的一個類,放到 Open J9 和 HotSpot 上面跑, HotSpot 立刻就報了一個格式錯誤,那麼 Open J9 是屬於一個正常執行,這裡面是因為沒有 main 函式,但是它總體算是一個正常透過的類。
後來我們就研究了差別原因,它的主要原因就是這裡面它有一個 flag,表明這個是一個介面檔案 interface。那麼從規範上來說,如果介面 flag 被設定了以後,它就要同時去設一個 abstract 的 flag,所以 HotSpot 報了一個格式問題,這個是正確的。那麼 Open J9 上我們就找到一個缺陷。
我們把這個問題其實也報給了 Open J9,經過了幾輪反覆,他們很快就修復了,修復完了以後又引入了新的問題,又修復,大概就是這樣的一個過程。
透過 classfuzz 這樣一個技術,甚至可以發現 Java 虛擬機器規範裡面的二義性。那麼左邊是這樣的一個類,它這裡面有個 public abstract{},它代表的是類初始化函式。我們或者是把某個方法名字改成了 clinit,或者是把正常的類初始化函式前面加了一個abstract。
那麼實際上 Open J9 和 HotSpot 又有了一個行為上的差異。我們回頭去看了一下原因,這個是因為 Java 虛擬機器規範的問題,Java 虛擬機器規範裡是這樣說的,other methods named <clinit> in a class file are of noconsequence, “除了類初始化這個函式以外,其他的函式加上這種識別符號 of no consequence”,這到底是一個什麼含義?
這個裡面大家就有誤解了,Hotspot 認為它是一個常規的方法,但是 J9 認為這裡面就是一個格式錯誤,這個就是大家對 of no consequence 會有認識上的不一樣。
Classfuzz 框架
接下來我來介紹一下 classfuzz 的框架,假設有個種子,進行了一個變種,變種結束以後,把變種類放到 Java7、Java8、Java9、J9、GCJ 上面一起去跑。那麼就可以透過一個類,生成了很多的變種檔案,在不同虛擬機器上面跑。
這個裡面其實想隨機的變種,隨機生成很多的變種類,效果非常差。於是又引入了這樣一個過程:有一個選擇和測試的過程,有很多的變種運算元,我們研究怎麼樣去選擇更有效的變種運算元,也選擇更加有代表性的一些類檔案來做測試。
那麼這裡面有幾個技術要點,由於時間限制,我就簡單過一下。
Classfuzz 的技術要點1
我們設計了 129 個變種運算元,其中 123 個是用來修改類的語法的,像我剛才說的 public 改成 private,刪掉一個名字,改掉一個函式名等等,或者刪掉一個函式等等。
我們還有 6 個修改語義,右邊是修改語義的一個簡單的辦法。我們採用一個 Java 位元組碼分析工具是 SOOT,這裡面它會把類轉成 Jimple 檔案,那麼對 Jimple 檔案的第 2 個語句和第 3 個語句,可以給它順序顛倒一下。但是這個顛倒效果沒有那麼理想,不是說所有程式的位元組碼都有一個先後關係。
Classfuzz 的技術要點 2
剛才說到有 129 個變種運算元,這個運算元數其實挺多的。我們的選擇性非常廣,那麼這裡面就採用了一個直覺,直覺是哪些變種運算元更加有效,就讓它用的更加頻繁一點,所以採用了一個有點偏機器學習的一個演算法,馬爾可夫鏈蒙特卡洛演算法來選擇更加有效的運算元。
我們預期會形成一個分佈,有些運算元給它一些高的機率,有些給低的機率。實際分佈不是所預期的這樣,但總體上趨勢還是比較接近的。
Classfuzz 的技術要點 3
會有很多的測試類會被生成,這個時候怎麼樣去選擇一些有代表性的測試類?我們採用了傳統測試裡面一個等價類劃分的技術,就把它們放到某個虛擬機器上去跑,放到 Hotspot 上面,特別是 classloader 那一塊程式碼,就收集一下它的行覆蓋率和分支覆蓋率,比較一下。
這個時候立刻就有一個數字上的感覺,假如這個數字不一樣,那麼就說明類在 Java 虛擬機器裡面的處理邏輯是不一樣的,如果處理邏輯不一樣,那麼就應該說兩個類特性還是不一樣的。
如果有新生成的類的話,拿到 Java 虛擬機器上跑,再來算一下它的覆蓋率,看看它是不是有代表性,這裡面代表性有兩個用途,第一個是用於多個 Java 虛擬機器差別測試,第二個是把它作為新的種子來做變種,能夠得到新的變種。
Classfuzz 的技術要點 4
第四個技術要點是差別測試,我們拿類到多個 Java 虛擬機器上跑,去觀察它們的執行結果,試圖去分析到底是在哪一個階段所丟擲的什麼問題。觀察在哪個階段報了錯,為什麼?當有幾個 JVM 的時候,就採用一個從眾原則推測哪個 Java 虛擬機器出錯了,這是差別測試的過程。
classfuzz 也發現了更多的 Java 虛擬機器的這種區別,這裡面有一個變數叫 R0,我們把 R0 的型別改了一下,從 map 改成 String,也發現了虛擬機器差別。我們還發現 J9 和 Hotspot 的驗證方式不一樣,當導進來一個類的時候,HotSpot 會把所有的方法都會驗證一遍,但是 J9 就顯得比較 lazy 一點,它只是對將來有可能執行的方法會去做一個驗證,所以這個時候也有一個差別。那麼此外還發現 GU 缺少維護,當然它現在更缺少維護了。
我們這個時候就意識到還會有很多的工作要做,於是就再接再厲,又做了下一個工作。classfuzz 並沒有能夠深入測試 Java 虛擬機器的底層,我們至始終在測的可能編譯那塊的同學比較感興趣一點,但是對於研究執行時的同學可能沒有那麼大的興趣,那麼主要的原因就是我們只是修改了語法,生成了很多格式正確或者不正確的位元組碼檔案.
但是去執行的時候,除了很少數的能夠修改語義操作的一些運算元以外,生成的大部分的東西或者是被拒了,或者是它的執行和前面的一些類沒有什麼差別。這個時候我們就思考這樣的一個事情,我們是不是能夠生成格式正確、但是語義不一樣的程式,語義不一樣也就是說你真的能夠在 Java 虛擬機器上跑,實際上語義不一樣的位元組碼。
這樣我們能夠測試兩個功能模組:第一個,驗證器;第二個,它的執行功能,或者執行引擎。大家覺得可能就有點意思了。好,在做這兩件事情的時候,其實有一些執念:
第一個執念,有很多同學都學了編譯,那麼編譯原理裡面其實有很多程式分析和最佳化的演算法。當時在做這件事情的時候,就很好奇,這麼多經典的演算法在 Java 虛擬機器實現當中,都正確地實現了嗎?我們能不能在實現裡面,找到一個實現錯了的一個演算法?
第二個執念,是不是能夠找到在兩個 Java 虛擬機器上執行結果不一樣的程式?這個典型的就拿主流的 J9 和 Hotspot,在上面能不能用同樣位元組碼,能夠執行不一樣,還有為什麼?比如執行的時候是不是還會有各種各樣奇怪的現象,例如 double free 等問題。
好,那麼接下來我們 show 一點例子,幫助大家瞭解 Java 虛擬機器的上述差別。
右邊一小段程式碼,那麼這兩段程式碼我們看是不是真的有什麼語義上的不一致?實際上左邊程式碼是建立了一個物件,從棧頂拿出了一個元素,做了一個比較,對吧?右邊程式碼表示從棧頂拿了一個元素,建立了一個物件,再做了比較。這兩個程式碼語義其實是一模一樣。
一個是 o 等於 this,一個是 this 等於 o。這兩段程式碼其實本質上都是錯誤程式碼,因為我們 new 完了以後其實沒有給物件初始化。但是到 Hotspot 和 J9 上面去執行的時候,Hotspot 給兩個都報了一個驗證錯誤,我們就發現,J9 在非常罕見的情況下,在某一個初始化函式里面,如果你寫了程式碼,它會透過驗證。實際上我們抓住了一個缺陷。這是一個比較簡單的例子。
那麼再來看比較複雜一點的例子,我們說資料流分析可能實現錯了,那能不能找一找執行結果不一樣的程式?右邊是一個種子類,先建立了一個物件,初始化,把這個物件設為空。接下來用 monitorenter 和 monitorexit。那麼 Jimple 正好反了一下。
把它轉換為類檔案以後,Hotspot 和 J9 它是比較一致的,Hotspot 丟擲了空指標異常,J9 也丟擲了空指標異常。這是因為 Java 虛擬機器規範裡面說,假如物件是空,我們遇到的第一個 R0,因為是空的,那麼它應該拋一個空指標異常。
那麼接下來看看到底怎麼樣去修改程式碼,發生了什麼?第一個乾的事情就是在裡面插入一個迴圈,直接跳到這,entermonitor R0,這個地方又做了一個迴圈回去,也就是說 entermonitorR0 會執行 20 遍, exitmonitor r0 執行了一遍。
這個時候我們發現這個 Hotspot 丟擲了一個 IMSE,但是 J9 是正常執行。追究原因,我們發現這裡面其實有一個叫結構鎖的機制,假如一個Java 虛擬機器要求去實現結構鎖這樣的一個機制,並且類違反了結構鎖規則,那麼就丟擲一個 IMSE,Hotspot 滿足結構鎖機制,但是 J9 不要求,所以這裡面會形成一個差別,這是所發現的第一個差別。
又去跑模糊測試,繼續去撞。這個時候從 new string,初始化之後,正好插入了一個 goto 語句到這,也就是說這是一個 new string r0,entermonitor r0,exitmonitor r0。Hotspot 就是正常的執行了,J9 就丟擲了一個驗證錯誤。HotSpot 反饋說這應該是一個正確的例子,因為雖然 R0 沒有初始化,但是這個裡面沒有什麼危害,所以就可以放過它。
那麼 J9 就認為它存在一個缺陷。實際上 Java 虛擬機器規範裡是這樣說的,一個驗證器如果遇上一個沒有初始化的物件,在使用的時候應該要報一個驗證問題。好,那麼既然到這種情況下面,entermonitor r0 它是使用了,就說明這個規則被違反了。又做了做,又撞了一個問題。entermonitor r0 這個東西是一個正常的物件,那麼 exitmonitor r0,這個時候 R0 是空物件,它本來應該匹配的,但是這個時候實際上變成空引用。
J9 丟擲了一個空指標異常,Hotspot 丟擲了 IMSE。J9 的解釋很合理,因為從規範上來說,monitorexit null 應該丟擲一個空指標異常。Hotspot 開發人員也找了很久,實際上發現在這做了一個最佳化,在這個時候 Hotspot 會丟擲幾個異常,但是這個時候會做一個最佳化,把其他異常都扔掉,留了一個 IMSE。
但是由於它們是拋的不一樣的異常,由於這些異常可以被分別捕獲,所以程式可以產生不同的執行結果。針對於同樣的一個種子,我們變化,會發現,這個程式它的執行還有點不太一樣。
這個裡面還發現了一些,比如說 Hotspot 的不同版本之間也會有一些差別,當我們的測試類比較複雜的時候,有控制流的歸併,有陣列的訪問,有異常處理等等,它會遇上一些問題。
技術方面,還是採用類變種,意圖是生成語義不一樣的一些檔案。怎麼樣算語義不一樣?也就是方法裡面能夠被執行到的位元組碼是不一樣的。那麼一個主要的思想就是要修改這個種子裡位元組碼。我們記錄哪些位元組碼被執行到了,在裡邊去修改一下,去修改它的控制流,那麼修改完了控制流以後,它的資料流也可能發生改變,這個時候會出現很異常的控制流或者資料流,如果我們把這種異常的情況放到 Java 虛擬機器上跑,有可能它的資料流分析會錯了,有可能其他情況也會出錯,這是很簡單的思想。
我們的技術要點,第一個會記錄一下哪些位元組碼會被執行到,反正做一些插裝就可以。第二個我們要做一些變種,在每個語句後面,就插入 goto。實際上除了 goto 之外,我們還可以插入 return、throw、lookupswitch,都是 Jimple 裡支援的。當然也可以去用 ASM 插入更多能夠修改控制流的指令。
變種過程也是一個頻繁試錯的過程,實際上遵循了一個流程,變種出錯我就把它拒了,變種過程就是有個種子,變完了以後,決定要接收、拒絕等等,得到新的類,繼續變種、接受、拒絕等。
我們差別測試主要是看看有什麼驗證問題,還有沒有可能會撞上系統崩潰,有沒有輸出差異,這種輸出差異並不是由併發導致的,而由 Java 虛擬機器實現上面的差異導致的。
最後介紹一個例子。右邊有一個函式,R2 等於 new string,那麼在這 R2 是一個物件,這個 R2 被用了,所以理論上他不能透過驗證,因為 R2 被使用之前沒有被初始化,違反了 Java 虛擬機器規範。
但是在這個裡面 Hotspot 成功地丟擲了驗證錯誤,但是 J9 沒有能夠拒絕,說明驗證器出錯了。實際上大家可以看一下這個問題是怎麼來的,其實就是植入了一個 goto,初始化中跳出去了,它正好 R2 就被使用了,這個時候就發現了這個問題。
總結
總結一下我們的工作,我們做了一個 Java 位元組碼變種及 Java 虛擬機器差別測試的一個技術方案,這個裡面可以暴露出 Java 虛擬機器的缺陷。
進一步我們希望去看看,既然有這麼多的變種,為什麼不把它應用到記憶體管理當中,看看記憶體管理有什麼問題,看看效能有什麼問題,特別是變種有可能會對一些高強度的計算,進行反覆的迭代,反覆計算,那麼是不是能夠發現效能方面的一些缺陷?本項工作合作者包括蘇黎世理工蘇振東教授、九州大學趙建軍教授、新加坡南洋理工蘇亭博士、谷歌公司孫誠年博士。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555606/viewspace-2374729/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深入淺出解讀 Java 虛擬機器的差別測試技術Java虛擬機
- 深入淺出Lua虛擬機器虛擬機
- 虛擬機器檢測技術攻防虛擬機
- 深入理解虛擬機器、容器和Hyper技術虛擬機
- 容器技術和虛擬機器技術的對比虛擬機
- 史上最深入淺出的IT術語解讀
- 技術乾貨 | AB測試和灰度釋出探索及實踐
- 反虛擬機器技術總結虛擬機
- 深入理解多執行緒(五)—— Java虛擬機器的鎖優化技術執行緒Java虛擬機優化
- 《深入理解Java虛擬機器》個人讀書總結——JAVA虛擬機器記憶體Java虛擬機記憶體
- Java 虛擬機器之一:Java 技術體系與平臺Java虛擬機
- 乾貨 | Dubbo 介面測試技術,測試開發進階必備
- 深入學習Java虛擬機器——虛擬機器位元組碼執行引擎Java虛擬機
- 《深入理解Java虛擬機器》讀書筆記Java虛擬機筆記
- 技術乾貨:關於效能測試面試題及答案面試題
- 《深入理解java虛擬機器》學習筆記4——Java虛擬機器垃圾收集器Java虛擬機筆記
- Linux雲端計算技術學習:跟蹤JAVA虛擬機器的垃圾回收LinuxJava虛擬機
- 區塊鏈技術概念深入淺出講解區塊鏈
- 技術乾貨收集
- 基於虛擬化技術的移動真機雲測試 - 澤眾
- 虛擬機器、容器與沙盒技術有什麼區別?虛擬機
- 深入理解java虛擬機器Java虛擬機
- 深度乾貨 | OceanBase 主動切主技術解讀
- LUA指令碼虛擬機器逃逸技術分析指令碼虛擬機
- 《深入理解java虛擬機器》學習筆記7——Java虛擬機器類生命週期Java虛擬機筆記
- 深入理解java虛擬機器——讀書筆記1Java虛擬機筆記
- 淺談GPU虛擬化技術(四)-GPU分片虛擬化GPU
- 淺談GPU虛擬化技術(四)- GPU分片虛擬化GPU
- Linux雲端計算技術學習:虛擬機器基本結構講解Linux虛擬機
- (深入理解 Java虛擬機器)一篇文章帶你深入瞭解Java 虛擬機器類載入器Java虛擬機
- 虛擬化技術之kvm虛擬機器建立工具qemu-kvm虛擬機
- 解讀JVM虛擬機器JVM虛擬機
- 深入淺出JVM(三)之HotSpot虛擬機器類載入機制JVMHotSpot虛擬機
- 《深入理解java虛擬機器》讀書筆記1(走近java)Java虛擬機筆記
- 深入淺出 Server-sent events 技術Server
- 在fedora中深入淺出VPN技術
- 虛擬機器遷移技術原理與應用虛擬機
- 深入理解Java虛擬機器(一)Java虛擬機