sds与二进制安全

在技术群里有人问了这么一个问题,redis的sds不是号称二进制安全吗?但是sdsnew函数里有对strlen进行调用啊。

跟着仔细看了一下源码,还真的是这样:

sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

但是说sds二进制安全是说错了么?也不是,我们看看sdslen这个函数:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

直接读取struct sdshdr就输出了,所以也是正常的人称时间复杂度O(1)的长度计算。

我们注意到上面的sdsnew还调用了sdsnewlen这个函数,也来看看它的定义:

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

看起来sdsnewlen这个初始化函数才是真正二进制安全的,那么我们怎么判断redis在set a "abc\x00d"值的时候能够保证这个二进制安全呢?

其实就是跟踪一下setCommand的运行流程啦。redis的这些command的命名也很规律,我们直接到setCommand的函数就可以。

/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;

    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
        // 循环里基本都是处理EX NX PX之类的和key value没关系的参数了,这里略过
    }

    // 重点在这里
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

最终key/value是调用setGenericCommand被塞到全局的dict里去,再看看setGenericCommand的定义,略去了不关心的部分:

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */
    // 这里
    setKey(c->db,key,val);
    server.dirty++;
}

跟着setKey一路下去,发现后面都是赋值操作,没有任何对sds类型的特殊操作。

所以是不是安全,需要从setGenericCommand向前找。首先是tryObjectEncoding,查看后发现只有sdslen,没有对strlen的调用。

看来只需要看从Client接收到的变量的最初的值了~

也就是最前面的那个c->argv[2]。

acceptCommonHandler => createClient => readQueryFromClient => processInputBuffer => processInlineBuffer

readQueryFromClient里的重点是:

    readlen = REDIS_IOBUF_LEN;

    // 读入内容到查询缓存
    nread = read(fd, c->querybuf+qblen, readlen);

整个过程就是网络编程里的简单read,tcp网络传输是没什么二进制安全问题的,因为读取结束的标准是端关闭连接发送EOF。这里之前的理解有些问题,从端读取过来的command内容是遵循redis协议的 RESP 中的 bulk 字符串数组,在 bulk string的传输过程中会先给出字符串的长度,所以读取的时候按照这个长度往后读即可,这里就不存在二进制安全的问题了。。

当内容完全读到client的querybuf(也是一个sds)中之后,调用processInputBuffer,识别客户端命令,构造输入参数。

int processInlineBuffer(redisClient *c) {
    // ....
    argv = sdssplitargs(aux,&argc);
    // ....
    return REDIS_OK;
}

终于定位到了最后一步,sdssplitargs。这个函数一堆ifelse不是很容易看,好在redis的作者另外提供了剥离的sds库(其实我们自己把redis的sds.c、sds.h、zmalloc拷贝出来也可以)。

简单写几个测试demo:

~/t/c/sds git:master ❯❯❯ cat binary_safe_test.c
#include "sds.h"
#include "stdio.h"
#include "string.h"

int main() {
    sds a = sdsnewlen("set a \"ohnot \\x00hisisthe value\"", 100);
    int *cnt;
    sds * arr = sdssplitargs(a, cnt);
    printf("argc is %d\n", *cnt);
    printf("argv[0] is %s\n", arr[0]);
    printf("argv[1] is %s\n", arr[1]);
    printf("argv[2] is %s\n", arr[2]);
    printf("sdslen is %zu\n", sdslen(arr[2]));
    printf("strlen is %lu\n", strlen(arr[2]));
}

~/t/c/sds git:master ❯❯❯ ./a.out
argc is 3
argv[0] is set
argv[1] is a
argv[2] is ohnot //因为是用%s打印,所以遇到\0就结束了
sdslen is 21
strlen is 6

确实没什么问题,字符串两边的引号也已经在存储的时候被处理掉了。

这么跟了半天,完全不见sdsnew的痕迹。还真是奇怪,写了一个sdsnew,那么为什么反而不会被使用呢?来grep一发,发现直接使用sdsnew的函数大多是些内部的字符串,或者是在hiredis的客户端,又或者是在测试程序里。

好吧orz。