*
* exec.cpp
* Functions for finding and validating executable files
*
*
* Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
* src/common/port/exec.cpp
*
* -------------------------------------------------------------------------
*/
#ifndef FRONTEND
#include "postgres.h"
#include "knl/knl_variable.h"
#else
#include "postgres_fe.h"
#endif
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "mb/pg_wchar.h"
#ifndef FRONTEND
#define log_error(str, param) ereport(LOG, (errmsg(str, param)))
#else
#define log_error(str, param) (fprintf(stderr, str, param), fputc('\n', stderr))
#endif
#ifdef WIN32_ONLY_COMPILER
#define getcwd(cwd, len) GetCurrentDirectory(len, cwd)
#endif
static int validate_exec(const char* path);
static int resolve_symlinks(char* path);
static char* pipe_read_line(const char* cmd, char* line, int maxsize);
#ifdef WIN32
static BOOL GetTokenUser(HANDLE hToken, PTOKEN_USER* ppTokenUser);
extern BOOL WINAPI AddAccessAllowedAceEx(PACL pAcl, DWORD dwAceRevision, DWORD AceFlags, DWORD AccessMask, PSID pSid);
#endif
* * @Description: check the value from environment variablethe to prevent command injection.
* * @in input_env_value : the input value need be checked.
* */
static bool check_env(const char* input_env_value)
{
#define MAXENVLEN 1024
const char* danger_character_list[] = {";", "`", "\\", "'", "\"", ">", "<", "$", "&", "|", "!", "\n", NULL};
int i = 0;
if (input_env_value == NULL || strlen(input_env_value) >= MAXENVLEN) {
log_error(_("wrong environment variable \"%s\""), input_env_value);
return false;
}
for (i = 0; danger_character_list[i] != NULL; i++) {
if (strstr((const char*)input_env_value, danger_character_list[i])) {
log_error(_("Error: environment variable \"%s\" contain invaild symbol\n"),
input_env_value);
return false;
}
}
return true;
}
* validate_exec -- validate "path" as an executable file
*
* returns 0 if the file is found and no error is encountered.
* -1 if the regular file "path" does not exist or cannot be executed.
* -2 if the file is otherwise valid but cannot be read.
*/
static int validate_exec(const char* path)
{
struct stat buf;
int is_r;
int is_x;
#ifdef WIN32
char path_exe[MAXPGPATH + sizeof(".exe") - 1];
if (strlen(path) >= strlen(".exe") && pg_strcasecmp(path + strlen(path) - strlen(".exe"), ".exe") != 0) {
errno_t rc = strncpy_s(path_exe, MAXPGPATH, path, MAXPGPATH - 1);
securec_check_c(rc, "\0", "\0");
rc = strcat_s(path_exe, MAXPGPATH + sizeof(".exe") - 1, ".exe");
securec_check_c(rc, "\0", "\0");
path = path_exe;
}
#endif
* Ensure that the file exists and is a regular file.
*
* XXX if you have a broken system where stat() looks at the symlink
* instead of the underlying file, you lose.
*/
if (stat(path, &buf) < 0) {
return -1;
}
if (!S_ISREG(buf.st_mode)) {
return -1;
}
* Ensure that the file is both executable and readable (required for
* dynamic loading).
*/
#ifndef WIN32
is_r = (access(path, R_OK) == 0);
is_x = (access(path, X_OK) == 0);
#else
is_r = buf.st_mode & S_IRUSR;
is_x = buf.st_mode & S_IXUSR;
#endif
return is_x ? (is_r ? 0 : -2) : -1;
}
* find_my_exec -- find an absolute path to a valid executable
*
* argv0 is the name passed on the command line
* retpath is the output area (must be of size MAXPGPATH)
* Returns 0 if OK, -1 if error.
*
* The reason we have to work so hard to find an absolute path is that
* on some platforms we can't do dynamic loading unless we know the
* executable's location. Also, we need a full path not a relative
* path because we will later change working directory. Finally, we want
* a true path not a symlink location, so that we can locate other files
* that are part of our installation relative to the executable.
*/
int find_my_exec(const char* argv0, char* retpath)
{
char cwd[MAXPGPATH], test_path[MAXPGPATH];
char* path = NULL;
errno_t rc;
if (getcwd(cwd, MAXPGPATH) == NULL) {
log_error(_("could not identify current directory: %s"), gs_strerror(errno));
return -1;
}
* If argv0 contains a separator, then PATH wasn't used.
*/
if (first_dir_separator(argv0) != NULL) {
if (is_absolute_path(argv0)) {
rc = strncpy_s(retpath, MAXPGPATH, argv0, MAXPGPATH - 1);
securec_check_c(rc, "\0", "\0");
} else {
join_path_components(retpath, cwd, argv0);
}
canonicalize_path(retpath);
if (validate_exec(retpath) == 0) {
return resolve_symlinks(retpath);
}
log_error(_("invalid binary \"%s\""), retpath);
return -1;
}
#ifdef WIN32
join_path_components(retpath, cwd, argv0);
if (validate_exec(retpath) == 0) {
return resolve_symlinks(retpath);
}
#endif
* Since no explicit path was supplied, the user must have been relying on
* PATH. We'll search the same PATH.
*/
if (((path = gs_getenv_r("PATH")) != NULL) && *path) {
char* startp = NULL;
char* endp = NULL;
do {
if (startp == NULL) {
startp = path;
} else {
startp = endp + 1;
}
endp = first_path_var_separator(startp);
if (endp == NULL) {
endp = startp + strlen(startp);
}
rc = strncpy_s(test_path, MAXPGPATH, startp, Min(endp - startp + 1, MAXPGPATH) - 1);
securec_check_c(rc, "\0", "\0");
if (!check_env(test_path)) {
break;
}
if (is_absolute_path(test_path)) {
join_path_components(retpath, test_path, argv0);
} else {
join_path_components(retpath, cwd, test_path);
join_path_components(retpath, retpath, argv0);
}
canonicalize_path(retpath);
switch (validate_exec(retpath)) {
case 0:
return resolve_symlinks(retpath);
case -1:
break;
case -2:
log_error(_("could not read binary \"%s\""), retpath);
break;
default:
break;
}
} while (*endp);
}
log_error(_("could not find a \"%s\" to execute"), argv0);
return -1;
}
* resolve_symlinks - resolve symlinks to the underlying file
*
* Replace "path" by the absolute path to the referenced file.
*
* Returns 0 if OK, -1 if error.
*
* Note: we are not particularly tense about producing nice error messages
* because we are not really expecting error here; we just determined that
* the symlink does point to a valid executable.
*/
static int resolve_symlinks(char* path)
{
#ifdef HAVE_READLINK
struct stat buf;
char tmp_path[MAXPGPATH] = {0};
char link_buf[MAXPGPATH] = {0};
int len = 0;
int rc = 0;
* And chdir() will affect all the threads within the process.
* So we have to avoid calling chdir() after main thread is working
* well in normal state.
*
* So we rewrite a new version resolve_symlinks() without calling
* chdir(). its function is tested well under SUSE and redhat system.
* Note: maybe some other platform/system fails to work.
*/
if (is_absolute_path(path)) {
len = strlen(path);
rc = memcpy_s(tmp_path, MAXPGPATH, path, len + 1);
securec_check_c(rc, "\0", "\0");
} else {
if (getcwd(tmp_path, MAXPGPATH) == NULL) {
log_error(_("could not identify current directory: %s"), gs_strerror(errno));
return -1;
}
join_path_components(tmp_path, tmp_path, path);
canonicalize_path(tmp_path);
}
for (;;) {
if (lstat(tmp_path, &buf) < 0 || !S_ISLNK(buf.st_mode)) {
break;
}
len = readlink(tmp_path, link_buf, sizeof(link_buf));
if (len < 0 || len >= (int)sizeof(link_buf)) {
log_error(_("could not read symbolic link \"%s\""), tmp_path);
return -1;
}
link_buf[len] = '\0';
if (is_absolute_path(link_buf)) {
rc = memcpy_s(tmp_path, MAXPGPATH, link_buf, len + 1);
securec_check_c(rc, "\0", "\0");
} else {
char* lsep = last_dir_separator(tmp_path);
*lsep = '\0';
join_path_components(tmp_path, tmp_path, link_buf);
canonicalize_path(tmp_path);
}
}
len = strlen(tmp_path);
rc = memcpy_s(path, MAXPGPATH, tmp_path, len + 1);
securec_check_c(rc, "\0", "\0");
#endif
return 0;
}
* Find another program in our binary's directory,
* then make sure it is the proper version.
*/
int find_other_exec(const char* argv0, const char* target, const char* versionstr, char* retpath)
{
char cmd[MAXPGPATH];
char line[150];
if (find_my_exec(argv0, retpath) < 0) {
return -1;
}
*last_dir_separator(retpath) = '\0';
canonicalize_path(retpath);
int sret = snprintf_s(
retpath + strlen(retpath), MAXPGPATH - strlen(retpath), MAXPGPATH - strlen(retpath) - 1, "/%s%s", target, EXE);
securec_check_ss_c(sret, "\0", "\0");
if (validate_exec(retpath) != 0) {
return -1;
}
int rc = snprintf_s(cmd, sizeof(cmd), sizeof(cmd) - 1, "\"%s\" -V", retpath);
securec_check_ss_c(rc, "\0", "\0");
if (pipe_read_line(cmd, line, sizeof(line)) == NULL) {
return -1;
}
#if (defined ENABLE_MULTIPLE_NODES) && (!defined ENABLE_LLT)
if (strcmp(line, versionstr) != 0) {
return -2;
}
#endif
return 0;
}
* The runtime library's popen() on win32 does not work when being
* called from a service when running on windows <= 2000, because
* there is no stdin/stdout/stderr.
*
* Executing a command in a pipe and reading the first line from it
* is all we need.
*/
static char* pipe_read_line(const char* cmd, char* line, int maxsize)
{
#ifndef WIN32
FILE* pgver = NULL;
fflush(stdout);
fflush(stderr);
errno = 0;
if ((pgver = popen(cmd, "r")) == NULL) {
perror("popen failure");
return NULL;
}
errno = 0;
if (fgets(line, maxsize, pgver) == NULL) {
if (feof(pgver)) {
fprintf(stderr, "no data was returned by command \"%s\"\n", cmd);
} else {
perror("fgets failure");
}
pclose(pgver);
return NULL;
}
if (pclose_check(pgver)) {
return NULL;
}
return line;
#else
SECURITY_ATTRIBUTES sattr;
HANDLE childstdoutrd = NULL;
HANDLE childstdoutwr = NULL;
HANDLE childstdoutrddup = NULL;
PROCESS_INFORMATION pi;
STARTUPINFO si;
char* retval = NULL;
sattr.nLength = sizeof(SECURITY_ATTRIBUTES);
sattr.bInheritHandle = TRUE;
sattr.lpSecurityDescriptor = NULL;
if (!CreatePipe(&childstdoutrd, &childstdoutwr, &sattr, 0)) {
return NULL;
}
if (!DuplicateHandle(GetCurrentProcess(),
childstdoutrd,
GetCurrentProcess(),
&childstdoutrddup,
0,
FALSE,
DUPLICATE_SAME_ACCESS)) {
CloseHandle(childstdoutrd);
CloseHandle(childstdoutwr);
return NULL;
}
CloseHandle(childstdoutrd);
ZeroMemory(&pi, sizeof(pi));
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdError = childstdoutwr;
si.hStdOutput = childstdoutwr;
si.hStdInput = INVALID_HANDLE_VALUE;
if (CreateProcess(NULL, (LPSTR)cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
char* lineptr = NULL;
ZeroMemory(line, maxsize);
for (lineptr = line; lineptr < line + maxsize - 1;) {
DWORD bytesread = 0;
if (WaitForSingleObject(childstdoutrddup, 10000) != WAIT_OBJECT_0) {
break;
}
if (!ReadFile(childstdoutrddup, lineptr, maxsize - (lineptr - line), &bytesread, NULL)) {
break;
}
lineptr += strlen(lineptr);
if (!bytesread) {
break;
}
if (strchr(line, '\n')) {
break;
}
}
if (lineptr != line) {
int len;
lineptr = strchr(line, '\n');
if (lineptr != NULL) {
*(lineptr + 1) = '\0';
}
len = strlen(line);
* If EOL is \r\n, convert to just \n. Because stdout is a
* text-mode stream, the \n output by the child process is
* received as \r\n, so we convert it to \n. The server main.c
* sets setvbuf(stdout, NULL, _IONBF, 0) which has the effect of
* disabling \n to \r\n expansion for stdout.
*/
if (len >= 2 && line[len - 2] == '\r' && line[len - 1] == '\n') {
line[len - 2] = '\n';
line[len - 1] = '\0';
len--;
}
* We emulate fgets() behaviour. So if there is no newline at the
* end, we add one...
*/
if (len == 0 || line[len - 1] != '\n') {
errno_t rc = strcat_s(line, len, "\n");
securec_check_c(rc, "\0", "\0");
}
retval = line;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
CloseHandle(childstdoutwr);
CloseHandle(childstdoutrddup);
return retval;
#endif
}
* pclose() plus useful error reporting
* Is this necessary? bjm 2004-05-11
* Originally this was stated to be here because pipe.c had backend linkage.
* Perhaps that's no longer so now we have got rid of pipe.c amd 2012-03-28
*/
int pclose_check(FILE* stream)
{
int exitstatus = pclose(stream);
if (exitstatus == 0) {
return 0;
}
if (exitstatus == -1) {
perror("pclose failed");
} else if (WIFEXITED(exitstatus)) {
log_error(_("child process exited with exit code %d"), WEXITSTATUS(exitstatus));
} else if (WIFSIGNALED(exitstatus)) {
#if defined(WIN32)
log_error(_("child process was terminated by exception 0x%X"), WTERMSIG(exitstatus));
}
#elif defined(HAVE_DECL_SYS_SIGLIST) && HAVE_DECL_SYS_SIGLIST
char str[256];
errno_t rc = snprintf_s(str,
sizeof(str),
sizeof(str) - 1,
"%d: %s",
WTERMSIG(exitstatus),
(WTERMSIG(exitstatus) < NSIG) ? sys_siglist[WTERMSIG(exitstatus)] : "(unknown)");
securec_check_ss_c(rc, "\0", "\0");
log_error(_("child process was terminated by signal %s"), str);
}
#else
log_error(_("child process was terminated by signal %d"), WTERMSIG(exitstatus));
}
#endif
else {
log_error(_("child process exited with unrecognized status %d"), exitstatus);
}
return -1;
}
* set_pglocale_pgservice
*
* Set application-specific locale and service directory
*
* This function takes the value of argv[0] rather than a full path.
*
* (You may be wondering why this is in exec.c. It requires this module's
* services and doesn't introduce any new dependencies, so this seems as
* good as anyplace.)
*/
void set_pglocale_pgservice(const char* argv0, const char* app)
{
char path[MAXPGPATH];
char my_exec_path[MAXPGPATH];
if (strcmp(app, PG_TEXTDOMAIN("gaussdb")) != 0) {
(void)setlocale(LC_ALL, "");
}
if (find_my_exec(argv0, my_exec_path) < 0) {
return;
}
#ifdef ENABLE_NLS
get_locale_path(my_exec_path, path);
bindtextdomain(app, path);
textdomain(app);
gs_setenv_r("PGLOCALEDIR", path, 0);
#endif
if (gs_getenv_r("PGSYSCONFDIR") == NULL) {
get_etc_path(my_exec_path, path, sizeof(path));
gs_setenv_r("PGSYSCONFDIR", path, 0);
}
}
#ifdef WIN32
* AddUserToTokenDacl(HANDLE hToken)
*
* This function adds the current user account to the restricted
* token used when we create a restricted process.
*
* This is required because of some security changes in Windows
* that appeared in patches to XP/2K3 and in Vista/2008.
*
* On these machines, the Administrator account is not included in
* the default DACL - you just get Administrators + System. For
* regular users you get User + System. Because we strip Administrators
* when we create the restricted token, we are left with only System
* in the DACL which leads to access denied errors for later CreatePipe()
* and CreateProcess() calls when running as Administrator.
*
* This function fixes this problem by modifying the DACL of the
* token the process will use, and explicitly re-adding the current
* user account. This is still secure because the Administrator account
* inherits its privileges from the Administrators group - it doesn't
* have any of its own.
*/
BOOL AddUserToTokenDacl(HANDLE hToken)
{
int i;
ACL_SIZE_INFORMATION asi;
ACCESS_ALLOWED_ACE* pace = NULL;
DWORD dwNewAclSize;
DWORD dwSize = 0;
const DWORD dwTokenInfoLength = 0;
PACL pacl = NULL;
PTOKEN_USER pTokenUser = NULL;
TOKEN_DEFAULT_DACL tddNew;
TOKEN_DEFAULT_DACL* ptdd = NULL;
TOKEN_INFORMATION_CLASS tic = TokenDefaultDacl;
BOOL ret = FALSE;
if (!GetTokenInformation(hToken, tic, (LPVOID)NULL, dwTokenInfoLength, &dwSize)) {
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
ptdd = (TOKEN_DEFAULT_DACL*)LocalAlloc(LPTR, dwSize);
if (ptdd == NULL) {
log_error(_("could not allocate %lu bytes of memory"), dwSize);
goto cleanup;
}
if (!GetTokenInformation(hToken, tic, (LPVOID)ptdd, dwSize, &dwSize)) {
log_error(_("could not get token information: error code %lu"), GetLastError());
goto cleanup;
}
} else {
log_error(_("could not get token information buffer size: error code %lu"), GetLastError());
goto cleanup;
}
}
if (ptdd != NULL &&
!GetAclInformation(ptdd->DefaultDacl, (LPVOID)&asi, (DWORD)sizeof(ACL_SIZE_INFORMATION), AclSizeInformation)) {
log_error(_("could not get ACL information: error code %lu"), GetLastError());
goto cleanup;
}
* Get the user token for the current user, which provides us with the SID
* that is needed for creating the ACL.
*/
if (!GetTokenUser(hToken, &pTokenUser)) {
log_error(_("could not get user token: error code %lu"), GetLastError());
goto cleanup;
}
dwNewAclSize = asi.AclBytesInUse + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pTokenUser->User.Sid) - sizeof(DWORD);
pacl = (PACL)LocalAlloc(LPTR, dwNewAclSize);
if (pacl == NULL) {
log_error(_("could not allocate %lu bytes of memory"), dwNewAclSize);
goto cleanup;
}
if (!InitializeAcl(pacl, dwNewAclSize, ACL_REVISION)) {
log_error(_("could not initialize ACL: error code %lu"), GetLastError());
goto cleanup;
}
for (i = 0; i < (int)asi.AceCount; i++) {
if (ptdd != NULL && !GetAce(ptdd->DefaultDacl, i, (LPVOID*)&pace)) {
log_error(_("could not get ACE: error code %lu"), GetLastError());
goto cleanup;
}
if (!AddAce(pacl, ACL_REVISION, MAXDWORD, pace, ((PACE_HEADER)pace)->AceSize)) {
log_error(_("could not add ACE: error code %lu"), GetLastError());
goto cleanup;
}
}
if (!AddAccessAllowedAceEx(pacl, ACL_REVISION, OBJECT_INHERIT_ACE, GENERIC_ALL, pTokenUser->User.Sid)) {
log_error(_("could not add access allowed ACE: error code %lu"), GetLastError());
goto cleanup;
}
tddNew.DefaultDacl = pacl;
if (!SetTokenInformation(hToken, tic, (LPVOID)&tddNew, dwNewAclSize)) {
log_error(_("could not set token information: error code %lu"), GetLastError());
goto cleanup;
}
ret = TRUE;
cleanup:
if (pTokenUser) {
LocalFree((HLOCAL)pTokenUser);
}
if (pacl) {
LocalFree((HLOCAL)pacl);
}
if (ptdd != NULL) {
LocalFree((HLOCAL)ptdd);
}
return ret;
}
* GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser)
*
* Get the users token information from a process token.
*
* The caller of this function is responsible for calling LocalFree() on the
* returned TOKEN_USER memory.
*/
static BOOL GetTokenUser(HANDLE hToken, PTOKEN_USER* ppTokenUser)
{
DWORD dwLength;
*ppTokenUser = NULL;
if (!GetTokenInformation(hToken, TokenUser, NULL, 0, &dwLength)) {
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
*ppTokenUser = (PTOKEN_USER)LocalAlloc(LPTR, dwLength);
if (*ppTokenUser == NULL) {
log_error(_("could not allocate %lu bytes of memory"), dwLength);
return FALSE;
}
} else {
log_error(_("could not get token information buffer size: error code %lu"), GetLastError());
return FALSE;
}
}
if (!GetTokenInformation(hToken, TokenUser, *ppTokenUser, dwLength, &dwLength)) {
LocalFree(*ppTokenUser);
*ppTokenUser = NULL;
log_error(_("could not get token information: error code %lu"), GetLastError());
return FALSE;
}
return TRUE;
}
#endif