关键词:fcgi、environ、getenv、段错误、segment fault、FCGI_Accept
fastcgi 更改环境变量environ引起的段错误(一)fcgi库代码分析
fastcgi 更改环境变量environ引起的段错误(二)修改libfcgi库源码解决getenv段错误问题
fastcgi 更改环境变量environ引起的段错误(三)getenv函数的使用
facgi库源码在网址https://github.com/FastCGI-Archives/fcgi2,遇到fcgi相关的问题时可以查看这个仓库的git commit信息,以确认新版本的库是否已解决所遇到的问题:
fastcgi在收到HTTP服务器(例如 nginx)新请求后,FCGI_Accept为全局变量stdin、stdout、stderr和environ 赋新值,即改变存放环境变量的全局变量 char **environ ,如果进程的线程使用 C库函数getenv()读取环境变量全局变量 char **environ会引起段错误。
在fcgi 进程中使用 mosquitto 创建客户端连接 mosquitto 订阅mqtt消息,如果需要重启 /usr/bin/mosquitto,这时fcgi 进程的"mosquitto loop" 线程会重新连接mosquitto,并调用函数getaddrinfo()->getenv(),由于fcgi的函数FCGI_Accept接收来自HTTP服务器的新请求时会修改存放环境变量的全局变量 char **environ ,如果同进程的线程"mosquitto loop"使用 C库函数getenv()读取环境变量全局变量 char **environ会引起段错误。
fastcgi在接收HTTP服务器(例如 nginx)请求前会先将上次创建的 static FCGX_Request the_request 清除, 其中在函数FreeParams(&request->paramsPtr) 中会执行 free(&request->paramsPtr->vec) ,也就是把全局变量 char **environ给 free了,这时再调用函数getenv()获取的 __environ 处存放的数据是非法地址,会引起段错误。
FCGI_Accept()先执行 FCGX_Finish_r(reqDataPtr)->FCGX_Free(reqDataPtr, close)->free(paramsPtr->vec) 先清除处理上次请求的数据,其中 paramsPtr->vec 赋值给全局变量 char **environ,在执行 free(paramsPtr->vec) 后,函数getenv()获取的全局变量 char **environ 指向的内存地址被free了,这时再使用getenv 再使用 environ 就会出现段错误。
接收到新请求后会在如下的调用过程中使用 malloc 分配堆空间,并让 全局变量 char **environ 指向这个堆空间:
- FCGX_Finish_r(reqDataPtr)-> //先清除处理上次请求的数据
- close |= FCGX_FClose(reqDataPtr->err);
- close |= FCGX_FClose(reqDataPtr->out);
- FCGX_Free(reqDataPtr, close)->
- FreeParams(&request->paramsPtr)->
- free(paramsPtr->vec); //paramsPtr->vec 赋值给全局变量 char **environ,free 之后再执行 getenv()将会出现段错误
- free(paramsPtr);
- reqDataPtr->paramsPtr = NewParams(30)->
- result = (Params *)Malloc(sizeof(Params));
- result->vec = (char **)Malloc(length * sizeof(char *));//paramsPtr->vec 赋值给全局变量 char **environ
在下面这个while循环体里范围内因为函数FCGI_Accept()已经对全局变量 char **environ地址进行了重新赋值,使用 getenv()是不会产生段错误的:
- while (FCGI_Accept() >= 0) { //函数FCGI_Accept 将修改了全局变量 char **environ地址
- cp = getenv("LOCALDOMAIN"); //在while循环体里执行 getenv() 没有问题,不会发生段错误
- }
而在fcgi处理完一次请求,并等待下一次请求期间,进程中的另一个线程调用getenv()就会产生段错误,因为函数FCGI_Accept() 在等待请求期间执行 free(paramsPtr->vec) ,函数getenv()获取的全局变量 char **environ 指向的内存地址被free了。
结构体变量 static FCGX_Request the_request 的定义如下:
- typedef struct FCGX_Request {
- int requestId; /* valid if isBeginProcessed */
- int role;
- FCGX_Stream *in;
- FCGX_Stream *out;
- FCGX_Stream *err;
- char **envp;
-
- /* Don't use anything below here */
- struct Params *paramsPtr;
- /*
- typedef struct Params {
- FCGX_ParamArray vec; /* vector of strings:存放 fcgi 所使用的环境变量,这个 vec 会赋值给 环境变量全局变量 char **environ*/
- int length; /* number of string vec can hold */
- char **cur; /* current item in vec; *cur == NULL */
- } Params;
- */
- int ipcFd; /* < 0 means no connection */
- int isBeginProcessed; /* FCGI_BEGIN_REQUEST seen */
- int keepConnection; /* don't close ipcFd at end of request */
- int appStatus;
- int nWriters; /* number of open writers (0..2) */
- int flags;
- int listen_sock;
- int detached;
- } FCGX_Request;
在网址FCGI_Accept.3-hshq_cn-ChinaUnix博客说明了函数FCGI_Accept 的用法:
FCGI_Accept的执行过程大致如下:
- int FCGI_Accept(void)->
- FCGX_ParamArray envp;
- int acceptResult = FCGX_Accept(&in, &out, &error, &envp);
- if (! libInitialized)
- {
- rc = FCGX_Init()->
- if (libInitialized) //已经初始化过则直接返回
- {
- return 0;
- }
- FCGX_InitRequest(&the_request, FCGI_LISTENSOCK_FILENO, 0); //static FCGX_Request the_request;
- memset(request, 0, sizeof(FCGX_Request));
- request->listen_sock = sock;
- request->flags = flags;
- request->ipcFd = -1;
- OS_LibInit(NULL)->
- char *libfcgiOsClosePollTimeoutStr = getenv( "LIBFCGI_OS_CLOSE_POLL_TIMEOUT" );
- char *libfcgiIsAfUnixKeeperPollTimeoutStr = getenv( "LIBFCGI_IS_AF_UNIX_KEEPER_POLL_TIMEOUT" );
- OS_InstallSignalHandlers(FALSE);
- libInitialized = TRUE;
- p = getenv("FCGI_WEB_SERVER_ADDRS");
- webServerAddressList = p ? StringCopy(p) : NULL;
- libInitialized = 1;
- }
- rc = FCGX_Accept_r(&the_request)-> //static FCGX_Request the_request;
- // Accepts a new request from the HTTP server.
- /*Creates a parameters data structure to be accessed via getenv(3) (if assigned to environ)
- or by FCGX_GetParam and assigns it to *envp.*/
- if (!libInitialized) {//已经初始化过则直接返回
- return -9998;
- }
- FCGX_Finish_r(reqDataPtr)-> //先清除处理上次请求的数据
- close |= FCGX_FClose(reqDataPtr->err);
- close |= FCGX_FClose(reqDataPtr->out);
- FCGX_Free(reqDataPtr, close)->
- FCGX_FreeStream(&request->in);
- FCGX_FreeStream(&request->out);
- FCGX_FreeStream(&request->err);
- FreeParams(&request->paramsPtr)->
- free(paramsPtr->vec); //paramsPtr->vec 赋值给全局变量 char **environ,free 之后再执行 getenv()将会出现段错误
- free(paramsPtr);
- reqDataPtr->in = NewReader(reqDataPtr, 8192, 0);
- reqDataPtr->paramsPtr = NewParams(30)->
- ParamsPtr result;
- result = (Params *)Malloc(sizeof(Params));
- result->vec = (char **)Malloc(length * sizeof(char *));//paramsPtr->vec 赋值给全局变量 char **environ
- result->length = length;
- result->cur = result->vec;
- *result->cur = NULL;
- SetReaderType(reqDataPtr->in, FCGI_STDIN); // 从 nginx 获取环境变量
- reqDataPtr->envp = reqDataPtr->paramsPtr->vec; //reqDataPtr->envp 会赋值给全局变量 char **environ
- *in = the_request.in;
- *out = the_request.out;
- *err = the_request.err;
- *envp = the_request.envp; //在这里对全局变量 char **environ 赋新的值
-
FCGI_Accept 会接收HTTP服务器(例如 nginx)传递过来的环境变量并进行设置,设置的环境变量如下:
FCGI_ROLE=RESPONDER
SCRIPT_FILENAME=/data/mxlApp/webserver/www/webserver.cgi
QUERY_STRING=config/getInfo
REQUEST_METHOD=POST
CONTENT_TYPE=
CONTENT_LENGTH=0
SCRIPT_NAME=/webserver.cgi
REQUEST_URI=/webserver.cgi?config/getInfo
DOCUMENT_URI=/webserver.cgi
DOCUMENT_ROOT=/data/mxlApp/webserver/www
SERVER_PROTOCOL=HTTP/1.1
REQUEST_SCHEME=http
GATEWAY_INTERFACE=CGI/1.1
SERVER_SOFTWARE=nginx/1.19.10
REMOTE_ADDR=192.168.1.147
REMOTE_PORT=62211
SERVER_ADDR=192.168.1.233
SERVER_PORT=80
SERVER_NAME=_
REDIRECT_STATUS=200
HTTP_HOST=192.168.1.233
HTTP_CONNECTION=keep-alive
HTTP_CONTENT_LENGTH=0
HTTP_USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
HTTP_ACCEPT=*/*
HTTP_ORIGIN=http://192.168.1.233
HTTP_REFERER=http://192.168.1.233/
HTTP_ACCEPT_ENCODING=gzip, deflate
HTTP_ACCEPT_LANGUAGE=zh-CN,zh;q=0.9
以下内容来自网址:FCGI_Accept.3-hshq_cn-ChinaUnix博客
摘要
#include "fcgi_stdio.h"
int
FCGI_Accept(void);
说明
FCGI_Accept函数接收来自HTTP服务器的新请求,并为之创建CGI兼容的执行环境。
如果应用作为CGI程序被调用,对FCGI_Accept的首次调用根本是个空操作,而且第二次调用返回-1。这使正确编码的FastCGI响应器应用运行单个请求并退出,产生CGI行为。
如果应用作为FastCGI服务器被调用,对FCGI_Accept的首次调用表明应用已经完成其初始化并且准备接受其第一个请求。对FCGI_Accept的 后续调用表明应用已经完成了对其当前请求的处理并准备接受新请求。 应用能通过调用FCGI_Finish(3)完成当前请求而不接受新请求;
随后,当准备接受新请求时,应用调用FCGI_Accept。
在完成当前请求期间,FCGI_Accept可以检测错误,例如到早前已经切断线路的客户端的断掉的管道。FCGI_Accept忽略这类错误。
希望处理这类错误的应用应当显式地调用fclose(stderr),然后fclose(stdout);任何一个返回EOF则表示一个错误。
当FCGI_Accept被调用时,如果环境变量FCGI_WEB_SERVER_ADDRS已经设置,后者应当包含逗号分隔的IP地址列表。每个IP地址写作由小数点分隔的0..255]区间内的四个十进制数。(nslookup(8)把更常见的符号, IP主机名(hostname),转换为该形式。)所以改变量的一个合法绑定是 FCGI_WEB_SERVER_ADDRS=199.170.183.28,199.170.183.71
FCGI_Accept为每个新线路的同级(peer)IP地址检查列表中的成员。如果检查失败(包括连路不是用的TCP/IP传输这种可能), FCGI_Accept关闭线路并接受另一个(二者之间并不返回)。
在收到新请求后,FCGI_Accept为全局变量stdin、stdout、stderr和environ 赋新值,即改变存放环境变量的全局变量 char **environ ,如果同进程的线程使用 C库函数getenv()读取环境变量全局变量 char **environ可以会引起段错误。在FCGI_Accept返回后,这些变量具有同CGI入口处一样的解释。
FCGI_Accept释放以前调用FCGI_Accept分配的任何内存。这很重要:
不要 保留指向数组或其中的任何字符串(例如调用getenv(3)的结果),因为这些将被下次调用FCGI_Finish或FCGI_Accept释放。
不要 使用setenv(3)或putenv(3)修改FCGI_Accept创建的environ数组,因为这将泄漏内存或致使下次调用FCGI_Finish或FCGI_Accept去释放不该释放的内存。
如果你的应用需要使用setenv或putenv修改environ数组,则它应当遵循这种编码模式:
char **savedEnviron, **requestEnviron;
int acceptStatus;
savedEnviron = environ;
acceptStatus = FCGI_Accept();
requestEnviron = environ;
environ = savedEnviron;
if(acceptStatus >= 0 && !FCGX_IsCGI()) {
/*
* requestEnviron指向FCGI_Accept分配的内存中的名-值对。
* 可以读取,不可保留指针 -- 改为获得拷贝。
*/
}
/*
* 可以setenv或putenv,但是小心内存泄漏!
*/
除了标准的CGI环境变量,环境变量FCGI_ROLE总是设为当前请求的角色。当前定义的角色有RESPONDER、AUTHORIZER和FILTER。
在FILTER角色中,也定义了额外的变量FCGI_DATA_LENGTH和FCGI_DATA_LAST_MOD。完备的信息可见man页FCGI_StartFilterData(3)。
提供的宏FCGI_ToFILE和FCGI_ToFcgiStream允许转义为使用类型FILE或FCGI_Stream的原生函数。在FILE的情形中,函数将必须被分别编译,因为fcgi_stdio.h用FCGI_FILE替换了标准的FILE。