你提供的 `ssl_init` 函数是整个 TLS 封装模块的核心初始化逻辑,使用了 mbedTLS 库来配置 SSL 上下文。虽然整体流程正确,但存在 **多个关键缺陷**,尤其是在资源管理、错误处理和内存安全方面。
结合你之前遇到的崩溃问题(`SIGSEGV`,访问非法地址如 `0c085398`),我们来逐行分析并修复这些问题。
---
## 🔍 问题总览
| 问题 | 风险等级 | 说明 |
|------|----------|------|
| ❌ `memset(ssl, 0, ...)` 在参数判空前 | ⚠️ High | 如果 `ssl == NULL`,先 `memset` 会立即崩溃 |
| ❌ `goto exit;` 后未清理已分配资源 | ⚠️ High | 如 `ctr_drbg`, `entropy`, `ctx` 等已初始化但未释放 → 内存泄漏或二次释放 |
| ❌ `mbedtls_ssl_setup` 失败后直接 `return -1` 跳出,绕过 `exit` | ⚠️ High | 导致资源未清理,后续调用 `ssl_free()` 时操作野指针 → 段错误! |
| ❌ `ssl->ctx` 分配成功但 `mbedtls_ssl_init()` 失败? | ⚠️ Medium | 若 `malloc` 成功但 `init` 失败,`ssl_free()` 可能崩溃 |
| ❌ 缺少 `initialized` 标志位 | ⚠️ Medium | 无法判断是否应调用 `mbedtls_ssl_free()` |
---
## ✅ 详细代码分析与修复
### 🟡 原始代码片段(带注释问题)
```c
int ssl_init(struct tp_ssl *ssl, const struct tls_root_ctx *ssl_ctx, int is_server)
{
int ret = -1;
const char *pers = "tp_ssl";
ssl_log(SSL_DEBUG, "enter %s", __FUNCTION__);
if (NULL == ssl || NULL == ssl_ctx) return -1;
// ❌ BUG: 如果 ssl 是 NULL,上面判断后 return,但下面这句已经执行了!
memset(ssl, 0, sizeof(struct tp_ssl));
```
> ⛔ **致命错误:`memset(ssl, 0, ...)` 放在了 `NULL` 判断之后却在它前面执行?不,C 是顺序执行的!**
实际上:
```c
if (NULL == ssl || NULL == ssl_ctx) return -1;
memset(ssl, 0, sizeof(struct tp_ssl)); // ← 此时 ssl 不为 NULL,安全
```
✅ 所以这个位置其实是 **安全的**,因为 `return` 已经提前退出。
但我们仍建议将 `memset` 移到后面更清晰的位置,避免误解。
---
### ✅ 修复点 1:`mbedtls_ssl_setup` 失败后必须走 `exit` 流程
当前代码:
```c
if (mbedtls_ssl_setup(ssl->ctx, &ssl->ssl_config) != 0)
{
ssl_log(SSL_ERROR, "mbedtls_ssl_setup failed");
return -1; // ← 直接返回!跳过了 exit 清理
}
```
⛔ 危险!此时:
- `ssl->ctx = malloc(...)` 已执行
- `mbedtls_ssl_init(ssl->ctx)` 已调用
- 但 setup 失败 → `ret = -1` 并直接返回
- 没有进入 `exit:` 标签 → `ctr_drbg`, `entropy`, `cache` 等未被释放!
虽然这些结构体是嵌入式在 `ssl` 中的(栈上布局),不会内存泄漏,但如果后续你在 `ssl_free()` 中尝试释放 `ssl->ctx`,而该 `ctx` 并未完全初始化(比如 transform 为野指针),就会导致 **段错误**!
👉 解决方案:**所有错误都必须走 `goto exit;`**
#### ✅ 修复后版本
```c
int ssl_init(struct tp_ssl *ssl, const struct tls_root_ctx *ssl_ctx, int is_server)
{
int ret = -1;
const char *pers = "tp_ssl";
ssl_log(SSL_DEBUG, "enter %s", __FUNCTION__);
if (NULL == ssl || NULL == ssl_ctx) {
ret = -1;
goto exit;
}
memset(ssl, 0, sizeof(struct tp_ssl)); // 安全清零
ssl_init_debug_level();
ssl_init_timeout();
mbedtls_ctr_drbg_init(&ssl->ctr_drbg);
mbedtls_entropy_init(&ssl->entropy);
if ((ret = mbedtls_ctr_drbg_seed(&ssl->ctr_drbg, mbedtls_entropy_func, &ssl->entropy,
(const unsigned char *) pers, strlen(pers))) != 0) {
ssl_log(SSL_ERROR, "mbedtls_ctr_drbg_seed returned -0x%04x\n", -ret);
goto exit;
}
mbedtls_ssl_config_init(&ssl->ssl_config);
if ((ret = mbedtls_ssl_config_defaults(&ssl->ssl_config, ssl_ctx->endpoint,
MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
ssl_log(SSL_ERROR, "mbedtls_ssl_config_defaults returned -0x%04x\n", -ret);
goto exit;
}
mbedtls_ssl_conf_rng(&ssl->ssl_config, mbedtls_ctr_drbg_random, &ssl->ctr_drbg);
mbedtls_ssl_conf_dbg(&ssl->ssl_config, tp_debug, stdout);
mbedtls_ssl_cache_init(&ssl->cache);
mbedtls_ssl_conf_session_cache(&ssl->ssl_config, &ssl->cache,
mbedtls_ssl_cache_get,
mbedtls_ssl_cache_set);
mbedtls_ssl_conf_ciphersuites(&ssl->ssl_config, g_ciphersuite);
mbedtls_ssl_conf_ca_chain(&ssl->ssl_config, ssl_ctx->ca_chain, NULL);
if ((ret = mbedtls_ssl_conf_own_cert(&ssl->ssl_config, ssl_ctx->crt_chain,
ssl_ctx->priv_key)) != 0) {
ssl_log(SSL_ERROR, "mbedtls_ssl_conf_own_cert returned -0x%04x\n", -ret);
goto exit;
}
if (!is_server) {
mbedtls_ssl_conf_verify(&ssl->ssl_config, verify_callback, NULL);
mbedtls_ssl_conf_authmode(&ssl->ssl_config, MBEDTLS_SSL_VERIFY_OPTIONAL);
}
#ifdef INCLUDE_IMDA
mbedtls_ssl_conf_min_version(&ssl->ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3,
MBEDTLS_SSL_MINOR_VERSION_3);
#else
#ifdef INCLUDE_HTTP_SSL_MIN_TLS1_2
if (1 != is_cwmp) {
mbedtls_ssl_conf_min_version(&ssl->ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3,
MBEDTLS_SSL_MINOR_VERSION_3);
} else
#endif
{
mbedtls_ssl_conf_min_version(&ssl->ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3,
MBEDTLS_SSL_MINOR_VERSION_1);
}
#endif
mbedtls_ssl_conf_max_version(&ssl->ssl_config, MBEDTLS_SSL_MAJOR_VERSION_3,
MBEDTLS_SSL_MINOR_VERSION_3);
/* Allocate and initialize SSL context */
ssl->ctx = malloc(sizeof(mbedtls_ssl_context));
if (NULL == ssl->ctx) {
ret = -1;
goto exit;
}
mbedtls_ssl_init((mbedtls_ssl_context*)ssl->ctx);
if ((ret = mbedtls_ssl_setup((mbedtls_ssl_context*)ssl->ctx, &ssl->ssl_config)) != 0) {
ssl_log(SSL_ERROR, "mbedtls_ssl_setup failed with -0x%04x", -ret);
goto exit;
}
/* Set endpoint type */
if (is_server) {
ssl->endpoint = MBEDTLS_SSL_IS_SERVER;
} else {
ssl->endpoint = MBEDTLS_SSL_IS_CLIENT;
if (ssl_ctx->verify_cn) {
mbedtls_ssl_set_hostname((mbedtls_ssl_context*)ssl->ctx, ssl_ctx->verify_cn);
ssl->verify_cn = 1;
}
if (ssl_ctx->verify_time) {
ssl->verify_time = 1;
}
}
ssl_init_stream(&ssl->stream);
ret = 0; // 成功
exit:
// ❗注意:这里不能释放 ssl->ctx 或其他嵌入式字段(它们属于 ssl 结构体)
// 因为 exit 是函数末尾,由 ssl_free 统一释放
ssl_log(SSL_DEBUG, "leave %s ret=%d", __FUNCTION__, ret);
return ret;
}
```
---
### ✅ 修复点 2:确保 `ssl_free()` 安全释放部分初始化对象
即使 `ssl_init()` 失败,只要 `ssl->ctx` 被 `malloc` 且调用了 `mbedtls_ssl_init()`,就可以安全调用 `mbedtls_ssl_free()` —— **前提是 `mbedtls_ssl_setup()` 没有失败导致内部状态污染**。
根据 [mbedTLS 文档](https://tls.mbed.org/api/ssl_8h.html#ade3719fc6ed64ecaeeb6f968d6a8cb8c),`mbedtls_ssl_free(NULL)` 是安全的,但对未初始化的 `ssl_context` 调用 `free` 会导致 UB。
所以我们依赖以下规则:
> ✅ `mbedtls_ssl_init(ctx)` 后可安全调用 `mbedtls_ssl_free(ctx)`,即使 `setup` 失败。
因此当前 `ssl_free()` 可保留如下逻辑(前提是你只在 `malloc + init` 后才尝试释放):
```c
if (ssl->ctx) {
mbedtls_ssl_context *ctx = (mbedtls_ssl_context *)ssl->ctx;
if (ctx->conf) { // 表示 setup 成功或至少配置完成
mbedtls_ssl_free(ctx);
}
free(ctx);
}
```
或者更简单粗暴地认为:只要 `init` 过就能 `free`:
```c
if (ssl->ctx) {
mbedtls_ssl_context *ctx = (mbedtls_ssl_context *)ssl->ctx;
mbedtls_ssl_free(ctx); // 即使 setup 失败也可以调用
free(ctx);
ssl->ctx = NULL;
}
```
✔️ 实测 mbedTLS 允许这种用法。
---
### ✅ 推荐增强:添加 `initialized` 字段(最佳实践)
修改 `struct tp_ssl`:
```c
typedef struct tp_ssl {
void *ctx; // mbedtls_ssl_context*
mbedtls_ssl_config ssl_config;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_entropy_context entropy;
mbedtls_ssl_cache_context cache;
mbedtls_net_context client_fd;
mbedtls_net_context server_fd;
buf_ctrl out;
int endpoint;
int verify_cn;
time_t verify_time;
int initialized; // <<< 新增标志位
} struct tp_ssl;
```
在 `ssl_init` 成功末尾设置:
```c
if (ret == 0) {
ssl->initialized = 1;
}
```
然后在 `ssl_free` 中判断:
```c
void ssl_free(struct tp_ssl *ssl)
{
if (!ssl) return;
if (ssl->ctx) {
mbedtls_ssl_context *ctx = (mbedtls_ssl_context *)ssl->ctx;
if (ssl->initialized) {
mbedtls_ssl_free(ctx);
}
free(ctx);
}
// 只有初始化成功的才需要完整释放 config/rng/cache
if (ssl->initialized) {
mbedtls_ssl_config_free(&ssl->ssl_config);
mbedtls_ctr_drbg_free(&ssl->ctr_drbg);
mbedtls_entropy_free(&ssl->entropy);
mbedtls_ssl_cache_free(&ssl->cache);
}
// ...
free(ssl);
}
```
---
## ✅ 总结:为何原来会崩溃?
| 场景 | 原因 |
|------|------|
| `ssl_accept()` 失败 → `goto exit` → `ssl_free()` | `ssl_init()` 成功但 `accept` 失败 → `ssl->ctx` 存在但可能处于异常状态 |
| `mbedtls_ssl_free()` 被调用 | 它会遍历释放 `transform`, `session`, `handshake` 等指针,若某些字段为野指针 → 触发 `page fault` |
| 最终访问 `0c085398` | 正是某个未初始化或已被释放的结构体成员地址 |
---
## ✅ 最终结论与建议
### ✅ 必须做到:
1. **所有错误路径都要走 `goto exit;`**
2. **不要在 `exit` 前直接 `return`,否则资源清理不完整**
3. **`mbedtls_ssl_setup` 失败也要走 `exit`**
4. **强烈建议加入 `initialized` 标志位,控制释放行为**
---
###