CryptDB代码分析2-handler与executor

之前已经介绍了SQL语句经过mysql-proxy的lua脚本与C++库交互的过程。在CryptDB的处理中,总体分为两个阶段:rewrite与next。本文介绍在rewrite和next这两个阶段中比较重要的两个类层次:handler以及executor。

SQL改写方式与query恢复介绍

首先考虑如何对SQL语句进行加密。CryptDB不直接处理字符串,而是借用了MySQL5.5版本的parser先对原始SQL语句进行解析,解析完以后获得一个LEX类型结构,是一个MySQL定义的类。 在加密阶段,对LEX内部的各个成员进行加密,并获得一个加密的LEX结构。 在恢复阶段,则需要将这个加密的LEX结构恢复成字符串类型,从而获得加密的SQL语句。

举例来说,对于一个SQL语句SELECT id from student, id 需要被加密。而这个语句解析成LEX 结构以后,id是在item_list成员里面,具体如下:

1
2
3

lex->select_lex.item_list

对这个item_list遍历,可以得到每个被选择的列对应的结构,是一个Item_field类型。其内部就包含了filed的名字,也就是id。加密过程,就是把这个内部的成员修改成自己想要的加密列的名字。完成LEX结构的加密以后,需要从LEX结构恢复成string类型的SQL语句,相关代码位于parser/stringify.hh。比如针对上面的SELECT语句,已经得到了加密以后的LEX结构,要重新得到字符串类型的SQL语句,可以通过如下的代码进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//代码来源 parser/stringfy.hh
//参数lex是加密以后的lex
static inline std::ostream&
operator<<(std::ostream &out, LEX &lex){
String s;
switch (lex.sql_command) {
//对于select语句,直接调用一次函数就可以恢复SQL语句
case SQLCOM_SELECT:
//string类型的结果保存在ostream类型的变量out里面
out << lex.unit;
break;
}
....
}

//该函数完成lex.unit到字符串的转化。
static inline std::ostream&
operator<<(std::ostream &out, SELECT_LEX_UNIT &select_lex_unit){
String s;
select_lex_unit.print(&s, QT_ORDINARY);
return out << s;
}

所以, 加密过程其实就是SQL语句的解析,以及解析以后的类的成员的修改。要理解完整的SQL解析,加密,以及LEX结构恢复成字符串的代码流程,就需要了解MySQL的parser的内部类的含义,这些在后续的文章中会逐步介绍。

SQL解析与分类处理

在mysqlproxy/ConnectWrapper.cc的rewrite函数中完成了基本的SQL加密操作。其内部执行SQL加密的入口是:Rewriter::rewrite函数。该函数的作用是获得一个QueryRewrite类,这个类包含了改写以后的SQL语句,以及数据解密所需要的元信息。其部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QueryRewrite
Rewriter::rewrite(const std::string &q, const SchemaInfo &schema,
const std::string &default_db, const ProxyState &ps) {
//辅助信息
Analysis analysis(default_db, schema, ps.getMasterKey(),
ps.defaultSecurityRating());
//SQL解析,加密,获得executor类型,在next阶段使用
AbstractQueryExecutor *const executor =
Rewriter::dispatchOnLex(analysis, q);
if (!executor) {
return QueryRewrite(true, analysis.rmeta, analysis.kill_zone,
new NoOpExecutor());
}
//QueryRewrite类包含所有必要信息
return QueryRewrite(true, analysis.rmeta, analysis.kill_zone, executor);
}

首先是调用Rewriter::dispatchOnLex函数, 获得executor, 然后返回QueryRewrite类。其中executor中就保存了加密以后的SQL语句。在dispatchOnLex函数中,首先调用MySQL的parer对原始的SQL语句进行解析,获得LEX结构, 然后根据lex.sql_command把SQL语句分为三类:noRewrite,ddl以及dml。不同类型的sql语句分配不同的handler类型进行处理,并返回executor类型,具体如下:

  • 对于noRewrite类型, 直接返回SimpleExecutor类型,其作用是直接返回明文的SQL语句,不进行任何的SQL改写操作。
  • 对于DML以及DDL,则分别由不同的handler类型对SQL语句进行处理,并且返回对应的executor结构。

其简化的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AbstractQueryExecutor *
Rewriter::dispatchOnLex(Analysis &a, const std::string &query) {
//使用MySQL的parser进行SQL解析,获得LEX类型
LEX *const lex = query_parse(query);
//三种不同的处理
if(noRewrite(*lex)){
return new SimpleExecutor();
}else if(dml_dispatcher->canDo(lex)){
const SQLHandler &handler = dml_dispatcher->dispatch(lex);
AbstractQueryExecutor executor = handler.transformLex(lex);
return executor;
}else if(ddl_dispatcher->canDo(lex)){
const SQLHandler &handler = ddl_dispatcher->dispatch(lex);
AbstractQueryExecutor executor = handler.transformLex(lex);
return executor;
}
}

可以看到,这里的加密处理涉及到dispatcher,handler,以及execotor。Dispatcher根据lex的结构,为SQL语句分配handler,不同类型的SQL语句有不同的handler,用来处理sql语句的加密。加密以后的结果则放置在executor中。dispatcher分配handler是基于map的查找来做的,其map初始化的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
SQLDispatcher *buildDMLDispatcher(){
DMLHandler *h;
SQLDispatcher *const dispatcher = new SQLDispatcher();
h = new InsertHandler();
dispatcher->addHandler(SQLCOM_INSERT, h);
h = new InsertHandler();
dispatcher->addHandler(SQLCOM_REPLACE, h);
h = new UpdateHandler;
dispatcher->addHandler(SQLCOM_UPDATE, h);
h = new DeleteHandler;
dispatcher->addHandler(SQLCOM_DELETE, h);
h = new MultiDeleteHandler;
dispatcher->addHandler(SQLCOM_DELETE_MULTI, h);
h = new SelectHandler;
dispatcher->addHandler(SQLCOM_SELECT, h);
h = new SetHandler;
dispatcher->addHandler(SQLCOM_SET_OPTION, h);
h = new ShowTablesHandlers;
dispatcher->addHandler(SQLCOM_SHOW_TABLES, h);
h = new ShowCreateTableHandler;
dispatcher->addHandler(SQLCOM_SHOW_CREATE,h);
return dispatcher;
}

SQLDispatcher *buildDDLDispatcher(){
DDLHandler *h;
SQLDispatcher *dispatcher = new SQLDispatcher();
h = new CreateTableHandler();
dispatcher->addHandler(SQLCOM_CREATE_TABLE, h);
h = new AlterTableHandler();
dispatcher->addHandler(SQLCOM_ALTER_TABLE, h);
h = new DropTableHandler();
dispatcher->addHandler(SQLCOM_DROP_TABLE, h);
h = new CreateDBHandler();
dispatcher->addHandler(SQLCOM_CREATE_DB, h);
h = new ChangeDBHandler();
dispatcher->addHandler(SQLCOM_CHANGE_DB, h);
h = new DropDBHandler();
dispatcher->addHandler(SQLCOM_DROP_DB, h);
h = new LockTablesHandler();
dispatcher->addHandler(SQLCOM_LOCK_TABLES, h);
h = new CreateIndexHandler();
dispatcher->addHandler(SQLCOM_CREATE_INDEX, h);
return dispatcher;
}
//初始化两个全局的dispatcher,内部通过map来保存handler结构
const std::unique_ptr<SQLDispatcher> Rewriter::dml_dispatcher =
std::unique_ptr<SQLDispatcher>(buildDMLDispatcher());
const std::unique_ptr<SQLDispatcher> Rewriter::ddl_dispatcher =
std::unique_ptr<SQLDispatcher>(buildDDLDispatcher());

在实际的代码中,上面的handler处理过程里还包含通过异常处理来调整洋葱层次的代码,这里暂时不做介绍。下面主要关注hander类以及executor类的组织方式以及特点。

三类handler

我们已经知道了通过不同的handler类,可以处理SQL语句,完成加密的操作。这里,首先给出Handler类的层次结构。

此外,对于ALTER TABLE语句,还有额外的subhandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

AlterDispatcher *buildAlterSubDispatcher() {
AlterDispatcher *dispatcher = new AlterDispatcher();
AlterSubHandler *h;

h = new AddColumnSubHandler();
dispatcher->addHandler(ALTER_ADD_COLUMN, h);

h = new DropColumnSubHandler();
dispatcher->addHandler(ALTER_DROP_COLUMN, h);

h = new ChangeColumnSubHandler();
dispatcher->addHandler(ALTER_CHANGE_COLUMN, h);

h = new ForeignKeySubHandler();
dispatcher->addHandler(ALTER_FOREIGN_KEY, h);

h = new AddIndexSubHandler();
dispatcher->addHandler(ALTER_ADD_INDEX, h);

h = new DropIndexSubHandler();
dispatcher->addHandler(ALTER_DROP_INDEX, h);

h = new DisableOrEnableKeys();
dispatcher->addHandler(ALTER_KEYS_ONOFF, h);

return dispatcher;
}

可以看到,不同类型的语句,有不同的handler来处理。这是因为,不同语句的处理流程是不一样的,我们分别来看如下三种handler的特点。

dml handler:

dml系列的handler类定义了如下的函数,其中gather和rewrite函数用于加密处理:

1
2
3
4
5
6
7
8
9
10
11
12
13

class DMLHandler : public SQLHandler {
public:
virtual AbstractQueryExecutor *
transformLex(Analysis &a, LEX *lex) const;
private:
virtual void gather(Analysis &a, LEX *lex) const = 0;
virtual AbstractQueryExecutor * rewrite(Analysis &a, LEX *lex) const = 0;
protected:
DMLHandler() {;}
virtual ~DMLHandler() {;}
};

每个dml系列的handler都实现了自己的gather和rewrite函数,在gather阶段,对于每个要改写的单元,保存一个rewrite plain,里面记录了这个基本单元可以用什么方式来进行加密。然后在rewrite阶段,使用这些rewrite plain,对解析以后的Lex结构中的基本单元做加密,从而完成加密功能。

ddl handler:

ddl 系列的handler类定义了如下的函数,用于加密处理。

1
2
3
4

virtual AbstractQueryExecutor *
rewriteAndUpdate(LEX *lex) const = 0;

ddl系列类型的典型处理流程包含了两部分,一是对SQL语句进行加密,二是以delta类来基于数据库的变化,这个delta在next阶段会写入到本地的embedded数据库中。例如CREATE TABLE语句,delta需要记录添加的表的名字, 表有多少列,每列分别采用什么样的加密算法。这些功能全都实现在rewriteAndUpdate函数中了。对于DML来说,由于不会对表结构产生影响,就不需要delta做记录。

Alter table handler

对于ALTER TABLE语句,由于其可能包含多个不同的子命令,所以创建了很多的不同的subhandler来进行处理,其调用subhandler的相关代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13

virtual AbstractQueryExecutor *
rewriteAndUpdate(Analysis &a, LEX *lex, const Preamble &pre) const {
//获取多个subhandler
const std::vector<AlterSubHandler *> &handlers =
sub_dispatcher->dispatch(lex);
assert(handlers.size() > 0);
// 使用多个subhandler对LEX进行加密处理
LEX *new_lex ;
for (auto it : handlers) {
new_lex = it->transformLex(a, new_lex);
}
}

可以看到,对于一个ALTER TABLE语句,存在多个subhandler的情况。这个信息保存在解析出来LEX结构的alter_info成员里面,通过位操作的方式,获得多个subhandler,分别进行加密处理。 部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//flags的可能值如下
lex->alter_info.flags;

#define ALTER_ADD_COLUMN (1L << 0)
#define ALTER_DROP_COLUMN (1L << 1)
#define ALTER_CHANGE_COLUMN (1L << 2)
#define ALTER_ADD_INDEX (1L << 3)
#define ALTER_DROP_INDEX (1L << 4)
#define ALTER_RENAME (1L << 5)
#define ALTER_ORDER (1L << 6)
#define ALTER_OPTIONS (1L << 7)
#define ALTER_CHANGE_COLUMN_DEFAULT (1L << 8)
#define ALTER_KEYS_ONOFF (1L << 9)
#define ALTER_CONVERT (1L << 10)
#define ALTER_RECREATE (1L << 11)
#define ALTER_ADD_PARTITION (1L << 12)
#define ALTER_DROP_PARTITION (1L << 13)
#define ALTER_COALESCE_PARTITION (1L << 14)
#define ALTER_REORGANIZE_PARTITION (1L << 15)
#define ALTER_PARTITION (1L << 16)
#define ALTER_ADMIN_PARTITION (1L << 17)
#define ALTER_TABLE_REORG (1L << 18)
#define ALTER_REBUILD_PARTITION (1L << 19)
#define ALTER_ALL_PARTITION (1L << 20)
#define ALTER_REMOVE_PARTITIONING (1L << 21)
#define ALTER_FOREIGN_KEY (1L << 22)
#define ALTER_TRUNCATE_PARTITION (1L << 23)

从handler到executor

executor 类型包含了很多的功能,其类型层次结构如下图所示。

其中主要的函数是:nextImpl。该函数一般基于boost 的coroutine机制来实现,将一个完整的功能分成几个部分。该函数在mysqlproxy/ConnectWrapper.cc 中的next函数里被调用。举例来说,对于SELECT语句,在mysqlproxy/ConnectWrapper.cc的rewrite阶段,得到的是DMLQueryExecutor。在mysqlproxy/ConnectWrapper.cc的next函数中,调用这个DMLQueryExecutor的nextImpl函数。第一次进入该函数的时候,返回加密以后的SQL。这个SQL传递给lua脚本,然后转发给MySQL处理并且获得加密以后的结果。在第二次进入nextImpl函数的时候,会对返回的加密结果进行解密,并将解密的结果返回给lua脚本,转发给客户端。这样,executor的函数执行,就可以和“CryptDB代码分析1-lua与加密库”中的介绍联系起来了。

一个非常简单的例子

介绍完了原理,现在给出一个非常简单的SQL语句加密的例子,将之前介绍的内容串联起来。我们考虑SHOW TABLES这个命令,该命令会获取当前db总的表名,由于表名是加密过的,所以mysql-proxy还是对表名进行解密才可以将结果返回给客户端。

首先,客户端发送SHOW TABLES命令的时候,被mysql-proxy接收到,并且调用mysqlproxy/ConnectWrapper.cc中的rewrite函数,其内部进入Rewriter::dispatchOnLex函数,首先调用parser进行解析,获得了LEX类型。然后进行判断。

1
2
3
4
5
6
7
8
9
10
if (noRewrite(*lex)) {


} else if (dml_dispatcher->canDo(lex)) {
const SQLHandler &handler = dml_dispatcher->dispatch(lex);
AbstractQueryExecutor * executor = handler.transformLex(a, lex);
} else if (ddl_dispatcher->canDo(lex)) {

}

由于是dml语句,所以进入了第二个分支,得到了一个DMLHandler,并且调用transformLex函数。在transformlext函数内部,包含了gather和rewrite两个函数,首先获得rewrite plain,然后根据rewrite plain多lex的内部成员进行改写,最后返回的executor,这里的executor类型是ShowTableExecutor。

1
2
3
4
5
6
7
8
9
10
class ShowTablesHandlers : public DMLHandler {
virtual void gather(Analysis &a, LEX *const lex) const
{
}

virtual AbstractQueryExecutor *rewrite(Analysis &a, LEX *lex) const
{
return new ShowTablesExecutor();
}
};

这样,rewrite阶段完成。在ShowTableExecutor中保存了加密以后的SQL语句。需要主要的是,由于show tables语句比较简单,没有加密,gather阶段没有获取任何的rewrite plain,rewrite阶段自然也就没有多lex的内部成员进行修改,所以加密以后的SQL语句依然是SHOW TABLES

然后到了next阶段,需要执行ShowTableExecutor中的nextImpl函数了,其实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ShowTablesExecutor::
nextImpl(const ResType &res, const NextParams &nparams)
{
reenter(this->corot) {
//返回加密以后的SQL语句,在这里是 SHOW TABLES
yield return CR_QUERY_AGAIN(nparams.original_query);
yield {
const std::shared_ptr<const SchemaInfo> &schema =
nparams.ps.getSchemaInfo();
const DatabaseMeta *const dm =
schema->getChild(IdentityMetaKey(nparams.default_db));
TEST_ErrPkt(dm, "failed to find the database '"
+ nparams.default_db + "'");
std::vector<std::vector<Item *> > new_rows;
for (const auto &it : res.rows) {
assert(1 == it.size());
for (const auto &table : dm->getChildren()) {
assert(table.second);
if (table.second->getAnonTableName()
== ItemToString(*it.front())) {

const IdentityMetaKey &plain_table_name
= dm->getKey(*table.second.get());
new_rows.push_back(std::vector<Item *>
{make_item_string(plain_table_name.getValue())});
}
}
}
//返回加密以后的表名
return CR_RESULTS(ResType(res, new_rows));
}
}
}

第一次调用的时候,返回了加密以后的命令,进入之前介绍的mysqlproxy/ConnectWrapper.cc中的next函数中的QUERY_COME_AGAIN分支,这个SQL语句传递给lua脚本,并转发给MySQL执行,然后返回加密的结果给mysql-proxy。之后会再次调用next函数,进入到ShowTablesExecutor::nextImpl函数,执行第二个return,返回解密以后的表名,并且进入RESULTS分支,这次就可以将解密以后的表名字返回给客户端,这样该SQL语句的执行就结束了。关于带rewrite plain的操作过程,涉及到更多复杂的处理,将在后续文章中给出。

总结

之前已经介绍过,在CryptDB的模型下,一个SQL语句通过mysqlproxy的lua脚本进行处理,其主要的处理函数是rewrite和next。rewrite被调用一次,用于SQL语句的加密,next则会被多次调用,和mysql-proxy交互,来完成数据解密等功能。本文首先介绍了rewrite阶段。 该阶段通过SQL解析获得LEX结构进行加密,然后将LEX结构恢复成字符串。由于不同类型的SQL有不同的加密方法,需要使用dispatcher类为其分配不同的handler。rewrite阶段结束以后,得到了executor类,其中就包含了加密以后的SQL以及其他相关的信息。executor类型的nextImpl函数在next阶段被调用,分阶段完成返回加密的SQL,数据解密等功能。

相关文献

正在开发新功能的CryptDB分支: https://github.com/yiwenshao/Practical-Cryptdb
https://yiwenshao.github.io/2018/02/26/CryptDB代码分析1-lua与加密库/