diff --git a/cmd/main.cpp b/cmd/main.cpp index 2159bec..c089fcf 100644 --- a/cmd/main.cpp +++ b/cmd/main.cpp @@ -23,15 +23,23 @@ int main(int argc, char **argv) { const FakeluaStateGuard guard; const auto s = guard.GetState(); - CompileConfig cfg; - cfg.debug_mode = FLAGS_debug; - CompileFile(s, argv[1], cfg); - int code = 0; - for (int i = 0; i < FLAGS_repeat; i++) { - Call(s, static_cast(FLAGS_jit_type), FLAGS_entry, code); - } - std::cout << "code: " << code << std::endl; + // Lua 惯例:未捕获的运行时错误打印消息后以退出码 1 结束,而不是让 C++ + // 异常穿透到 libc++abi 触发 SIGABRT(退出码 134,FakeLua Phase16 Bug 4)。 + try { + CompileConfig cfg; + cfg.debug_mode = FLAGS_debug; + CompileFile(s, argv[1], cfg); + + int code = 0; + for (int i = 0; i < FLAGS_repeat; i++) { + Call(s, static_cast(FLAGS_jit_type), FLAGS_entry, code); + } + std::cout << "code: " << code << std::endl; - return code; + return code; + } catch (const std::exception &e) { + std::cerr << "flua: " << e.what() << std::endl; + return 1; + } } diff --git a/src/native/README.md b/src/native/README.md index eabbc43..2ff5fe5 100644 --- a/src/native/README.md +++ b/src/native/README.md @@ -667,6 +667,8 @@ Async client on Boost.Redis. Reconnect and health-checks are off so a failed con Connect does not send `HELLO 3`, so Redis 4+ works; AUTH/SELECT are issued when `password` / `db` are set. Arrays become Lua arrays; RESP3 maps become tables; bulk/simple strings stay strings. After an optional `HELLO 3`, `HGETALL` is a map. +> **Pub/Sub is not supported**: `command()` is a one-shot RPC channel (one callback per command) and cannot deliver the continuous pushes a subscription produces; `SUBSCRIBE`/`PSUBSCRIBE`/`UNSUBSCRIBE`/`PUNSUBSCRIBE`/`SSUBSCRIBE`/`SUNSUBSCRIBE` raise an error. For message delivery use polling (e.g. `LPUSH`+`LRANGE`, `ZADD`+`ZREVRANGE`). + --- ## SQLite diff --git a/src/native/README.zh.md b/src/native/README.zh.md index 3198bc8..47d7410 100644 --- a/src/native/README.zh.md +++ b/src/native/README.zh.md @@ -722,6 +722,8 @@ end 连接时不发送 `HELLO 3`,因此 Redis 4+ 可用;配置了 `password` / `db` 时会发 AUTH/SELECT。数组变成 Lua 数组,RESP3 map 变成表,字符串保持字符串。可选执行 `HELLO 3` 后,`HGETALL` 为 map。 +> **不支持 Pub/Sub**:`command()` 是一次性 RPC 通道(每条命令对应一次回调),无法承接订阅后的持续推送;`SUBSCRIBE`/`PSUBSCRIBE`/`UNSUBSCRIBE`/`PUNSUBSCRIBE`/`SSUBSCRIBE`/`SUNSUBSCRIBE` 会直接报错。需要消息推送请用拉模式(如 `LPUSH`+`LRANGE`、`ZADD`+`ZREVRANGE`)。 + --- ## SQLite diff --git a/src/native/math/native_math.cpp b/src/native/math/native_math.cpp index d95f972..8954e8c 100644 --- a/src/native/math/native_math.cpp +++ b/src/native/math/native_math.cpp @@ -5,8 +5,10 @@ #include #include #include +#include #include #include +#include namespace fakelua::math { @@ -67,11 +69,30 @@ static int64_t RandomInt64() { return std::bit_cast(value); } +// Lua 5.4 在状态初始化时自动播种 PRNG。底层 std::rand 全局唯一且默认种子恒为 1, +// 不显式 math.randomseed 时每次进程启动的随机序列完全相同(FakeLua Phase16 Bug 3)。 +// 进程内只播一次;显式 math.randomseed(x) 之后仍以用户种子为准。 +static void AutoSeedRandomOnce() { + static const bool seeded = [] { + uint64_t seed = static_cast(std::time(nullptr)); + std::random_device rd; + seed ^= (static_cast(rd()) << 32) | static_cast(rd()); + // 栈地址熵:同一秒内启动的进程也不易撞种子。 + int stack_anchor = 0; + seed ^= static_cast(reinterpret_cast(&stack_anchor)); + std::srand(static_cast(seed)); + return true; + }(); + (void) seeded; +} + // Use shared CheckNumberArg from native_common.h void RegisterMathLibraryApi(State *s) { if (!s) return; + AutoSeedRandomOnce(); + RegisterNativeFunction(s, "math.abs", 1, false, [](State *state, CVar *args, int n) -> CVar { CVar a0 = inter::GetNativeArg(state, args, n, 0); CheckMathNumberArg(a0, 1, "math.abs"); diff --git a/src/native/redis/native_redis.cpp b/src/native/redis/native_redis.cpp index 6b71411..c198c67 100644 --- a/src/native/redis/native_redis.cpp +++ b/src/native/redis/native_redis.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -174,10 +175,23 @@ class RedisConnection { } void Command(std::vector parts, std::string cb) { + if (parts.empty()) ThrowFakeluaException("redis command: empty argv"); + // Pub/Sub 订阅族命令不能走一次性 RPC 通道:订阅成功后 hiredis 会对每条推送 + // 都以同一个 privdata 回调 OnCommand,而该 CmdOp 在首条 ack 派发后即被弹出 + // 销毁,第二条推送起就是悬垂指针写(use-after-free)。完整支持需要独立的长 + // 生命周期订阅上下文;在此之前响亮拒绝,避免堆破坏(FakeLua Phase16 Bug 2)。 + // 参数校验先于连接状态检查,未就绪的连接上也能拿到明确报错。 + std::string verb = parts.front(); + std::transform(verb.begin(), verb.end(), verb.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + if (verb == "subscribe" || verb == "psubscribe" || verb == "unsubscribe" || verb == "punsubscribe" || + verb == "ssubscribe" || verb == "sunsubscribe") { + ThrowFakeluaException("redis: '" + verb + "' is not supported, command() is one-shot RPC and cannot deliver pub/sub pushes"); + } if (closed_ || close_pending_ || !ac_ || !ready_) { ThrowFakeluaException("redis: connection is closed"); } - if (parts.empty()) ThrowFakeluaException("redis command: empty argv"); auto op = std::make_unique(); op->cb = std::move(cb); auto *raw = op.get(); diff --git a/test/lua/redis/test_redis.lua b/test/lua/redis/test_redis.lua index 0bb9258..2b7ea20 100644 --- a/test/lua/redis/test_redis.lua +++ b/test/lua/redis/test_redis.lua @@ -71,3 +71,25 @@ function test_bad_db() if ok then return 0 end return 1 end + +-- SUBSCRIBE/PSUBSCRIBE/UNSUBSCRIBE/PUNSUBSCRIBE 必须被响亮拒绝:一次性 RPC 命令 +-- 通道无法承接 Pub/Sub 推送,放行会导致第二条推送写已释放的 CmdOp(UAF)。 +-- 无需真实服务器:参数校验先于连接就绪检查,错误可被 pcall 捕获。 +function test_subscribe_rejected() + local config = {} + config["host"] = "127.0.0.1" + config["port"] = 1 + config["timeout_ms"] = 500 + local conn = redis.connect(config, "RedisTest.on_connect") + local cmds = { "SUBSCRIBE", "PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "subscribe", "SSUBSCRIBE", "SUNSUBSCRIBE", "ssubscribe" } + for i = 1, #cmds do + local cmd = cmds[i] + local ok, err = pcall(function() + conn:command({ cmd, "ch" }, "RedisTest.on_connect") + end) + if ok then conn:close(); return 0 end + if not string.find(tostring(err), "not supported") then conn:close(); return 0 end + end + conn:close() + return 1 +end diff --git a/test/test_redis.cpp b/test/test_redis.cpp index 3e31d21..8aba3e5 100644 --- a/test/test_redis.cpp +++ b/test/test_redis.cpp @@ -122,3 +122,15 @@ TEST(test_redis, bad_db) { EXPECT_EQ(ret, 1); FakeluaDeleteState(s); } + +// Pub/Sub 订阅族命令被拒绝(防 use-after-free,无需真实服务器) +TEST(test_redis, subscribe_rejected) { + State *s = FakeluaNewState(); + ASSERT_NE(s, nullptr); + CompileConfig config; + CompileFile(s, "./redis/test_redis.lua", config); + int64_t ret = 0; + CallAll(s, "RedisTest.test_subscribe_rejected", ret); + EXPECT_EQ(ret, 1); + FakeluaDeleteState(s); +}