天天看點

Memcached 二進制協定(BinaryProtocol) incr指令洩露記憶體資料的bug

緣起

最近有個分布式限速的需求。支付寶的接口雙11隻允許每秒調用10次。

單機的限速,自然是用google guava的RateLimiter。

http://docs.guava-libraries.googlecode.com/git-history/master/javadoc/com/google/common/util/concurrent/RateLimiter.html

分布式的ReteLimiter,貌似沒有現在的實作方案。不過用memcached或者Redis來實作一個簡單的也很快。

比如上面的要求,每秒鐘隻允許調用10次,則按下面的流程來執行,以memcached為例:

incr alipay_ratelimiter  1 1
如果傳回NOT_FOUND,則
  add alipay_ratelimiter  0  1 1
  1
  即如果alipay_ratelimiter不存在,則設定alipay_ratelimiter的值為1,過期時間為1秒。
如果incr傳回不是具體的數值,則判斷是否大于10,
如果大于10則要sleep等待。           

上面是Memcached 文本協定的做法。因為文本協定不允許incr 設定不存在的key。

如果是二進制協定,則可以直接用incr指令設定初始值,過期時間。

memcached二進制協定的bug

上面扯遠了,下面來說下memcached incr指令的bug。

在測試的時間,用XMemcached做用戶端,來測試,發現有的時候,incr函數傳回兩個1。

于是,在指令行,用telnet來測試,結果發現有時候傳回很奇怪的資料:

get alipay_ratelimiter
VALUE alipay_ratelimiter 0 22
END2446744073709551608           

明顯END後面跟了一些很奇怪的資料。而且傳回資料的長度是22,而正确的長度應該是1。

正常的傳回應該是這樣的:

get alipay_ratelimiter
VALUE alipay_ratelimiter 0 4
1
END           

抓包分析

開始以為是XMemcached用戶端的bug,也有可能是序列化方式有問題。于是調試了下代碼,沒發現什麼可疑的地方。

于是祭出wireshakr來抓包。發現XMemcached發出來的資料包是正常的。而且伺服器的确傳回了22位元組的資料。

那麼這個可能是Memcached本身的bug了。這個令人比較驚奇,因為Memcached本身已經開發多年,很穩定了,怎麼會有這麼明顯的bug?

查找有問題的Memcached的版本

檢查下目前的Memcahcd版本,是memcached 1.4.14。

于是去下載下傳了最新的1.4.21版,編繹安裝之後,再次測試。發現正常了。

于是到release log裡檢視是哪個版本修複了:

https://code.google.com/p/memcached/wiki/ReleaseNotes

發現1417版的release note裡有incr相關的資訊:

https://code.google.com/p/memcached/wiki/ReleaseNotes1417

Fix for incorrect length of initial value set via binary increment protocol.

查找bug發生的原因:

于是再到github上檢視修改了哪些内容:

https://github.com/memcached/memcached/commit/8818bb698ea0abd5199b2792964bbc7fbe4cd845?diff=split

對比下修改内容,和檢視下源代碼,可以發現,其實是開發人員在為incr指令存儲的資料配置設定記憶體時,沒有注意邊界,一下子配置設定了INCR_MAX_STORAGE_LEN,即24位元組的記憶體,卻沒有正常地設定'\r\n'到真實資料的最後。是以當Get請求拿到資料是,會把22位元組 + "\r\n"的資料傳回給使用者,造成了記憶體資料洩露。

修複前的代碼:

it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
                            INCR_MAX_STORAGE_LEN);

            if (it != NULL) {
                snprintf(ITEM_data(it), INCR_MAX_STORAGE_LEN, "%llu",
                         (unsigned long long)req->message.body.initial);           

修複後的代碼:

snprintf(tmpbuf, INCR_MAX_STORAGE_LEN, "%llu",
                (unsigned long long)req->message.body.initial);
            int res = strlen(tmpbuf);
            it = item_alloc(key, nkey, 0, realtime(req->message.body.expiration),
                            res + 2);

            if (it != NULL) {
                memcpy(ITEM_data(it), tmpbuf, res);
                memcpy(ITEM_data(it) + res, "\r\n", 2);           

為什麼這個bug隐藏了這麼久

從測試的版本可以看到,至少從12年這個bug就存在了,從github上的代碼來看,09年之前就存在了。直到13年12月才被修複。

為什麼這個bug隐藏了這個久?可能是因為傳回的22位元組資料裡中間有正确的加了\r\n,後面的才是多餘的洩露資料,可能大部分解析庫都以"\r\n"為分隔,進而跳過了解析到的多餘的資料。比如XMemcached就能解析到。

另外,隻有混用二進制協定和文本協定才可能會發現。

估計有不少伺服器上運作的memcached版本都是比1.4.17要老的,比如ubuntu14預設的就是1.4.14。

這個bug的危害

不過這個bug的危害比較小,因為洩露的隻有20個位元組的資料。對于一些雲服務指供的cache服務,即使後面是memcached做支援,也會有一個中轉的路由。

竊取到有效資訊的可能性很小。

其它的一些東東

wireshark設定解析memcached協定:

wireshark預設是支援解析memcached文本和二進制協定的,不過預設解析端口是11211,是以如果想要解析其它端口的包,要設定下。

在"Edit", ”Preferences“ 裡,找到Memcached協定,就可以看到端口的配置了。

參考:https://ask.wireshark.org/questions/24495/memcache-and-tcp 

XMemcached的文本協定incr指令的實作:

上面說到Memcached的文本協定是不支援incr設定不存在的key的,但是XMemcached卻提供了相關的函數,而且能正常運作,是為什麼呢?

/**
	 * "incr" are used to change data for some item in-place, incrementing it.
	 * The data for the item is treated as decimal representation of a 64-bit
	 * unsigned integer. If the current data value does not conform to such a
	 * representation, the commands behave as if the value were 0. Also, the
	 * item must already exist for incr to work; these commands won't pretend
	 * that a non-existent key exists with value 0; instead, it will fail.This
	 * method doesn't wait for reply.
	 * 
	 * @param key
	 *            key
	 * @param delta
	 *            increment delta
	 * @param initValue
	 *            the initial value to be added when value is not found
	 * @param timeout
	 *            operation timeout
	 * @param exp
	 *            the initial vlaue expire time, in seconds. Can be up to 30
	 *            days. After 30 days, is treated as a unix timestamp of an
	 *            exact date.
	 * @return
	 * @throws TimeoutException
	 * @throws InterruptedException
	 * @throws MemcachedException
	 */
	long incr(String key, long delta, long initValue, long timeout, int exp)
			throws TimeoutException, InterruptedException, MemcachedException;           

實際上,XMemcached内部包裝了incr和add指令,表明上是調用了incr但實際上是兩條指令:

incr alipay_ratelimiter 1
NOT_FOUND
add alipay_ratelimiter 0 0 1
1
STORED           

另外,要注意XMemcached預設是文本協定的,隻有手動配置,才會使用二進制協定。

Memcached二進制協定比文本協定要快多少?

據這個示範的結果,二進制協定比文本協定要略快,第14頁:

http://www.slideshare.net/tmaesaka/memcached-binary-protocol-in-a-nutshell-presentation/

參考:

https://code.google.com/p/memcached/wiki/MemcacheBinaryProtocol 

二進制協定介紹的ppt:

繼續閱讀