PostgreSQL Source Code  git master
exec.c
Go to the documentation of this file.
1 /*-------------------------------------------------------------------------
2  *
3  * exec.c
4  * Functions for finding and validating executable files
5  *
6  *
7  * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
8  * Portions Copyright (c) 1994, Regents of the University of California
9  *
10  *
11  * IDENTIFICATION
12  * src/common/exec.c
13  *
14  *-------------------------------------------------------------------------
15  */
16 
17 #ifndef FRONTEND
18 #include "postgres.h"
19 #else
20 #include "postgres_fe.h"
21 #endif
22 
23 #include <signal.h>
24 #include <sys/stat.h>
25 #include <sys/wait.h>
26 #include <unistd.h>
27 
28 /*
29  * Hacky solution to allow expressing both frontend and backend error reports
30  * in one macro call. First argument of log_error is an errcode() call of
31  * some sort (ignored if FRONTEND); the rest are errmsg_internal() arguments,
32  * i.e. message string and any parameters for it.
33  *
34  * Caller must provide the gettext wrapper around the message string, if
35  * appropriate, so that it gets translated in the FRONTEND case; this
36  * motivates using errmsg_internal() not errmsg(). We handle appending a
37  * newline, if needed, inside the macro, so that there's only one translatable
38  * string per call not two.
39  */
40 #ifndef FRONTEND
41 #define log_error(errcodefn, ...) \
42  ereport(LOG, (errcodefn, errmsg_internal(__VA_ARGS__)))
43 #else
44 #define log_error(errcodefn, ...) \
45  (fprintf(stderr, __VA_ARGS__), fputc('\n', stderr))
46 #endif
47 
48 #ifdef _MSC_VER
49 #define getcwd(cwd,len) GetCurrentDirectory(len, cwd)
50 #endif
51 
52 static int validate_exec(const char *path);
53 static int resolve_symlinks(char *path);
54 
55 #ifdef WIN32
56 static BOOL GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser);
57 #endif
58 
59 /*
60  * validate_exec -- validate "path" as an executable file
61  *
62  * returns 0 if the file is found and no error is encountered.
63  * -1 if the regular file "path" does not exist or cannot be executed.
64  * -2 if the file is otherwise valid but cannot be read.
65  */
66 static int
67 validate_exec(const char *path)
68 {
69  struct stat buf;
70  int is_r;
71  int is_x;
72 
73 #ifdef WIN32
74  char path_exe[MAXPGPATH + sizeof(".exe") - 1];
75 
76  /* Win32 requires a .exe suffix for stat() */
77  if (strlen(path) >= strlen(".exe") &&
78  pg_strcasecmp(path + strlen(path) - strlen(".exe"), ".exe") != 0)
79  {
80  strlcpy(path_exe, path, sizeof(path_exe) - 4);
81  strcat(path_exe, ".exe");
82  path = path_exe;
83  }
84 #endif
85 
86  /*
87  * Ensure that the file exists and is a regular file.
88  *
89  * XXX if you have a broken system where stat() looks at the symlink
90  * instead of the underlying file, you lose.
91  */
92  if (stat(path, &buf) < 0)
93  return -1;
94 
95  if (!S_ISREG(buf.st_mode))
96  return -1;
97 
98  /*
99  * Ensure that the file is both executable and readable (required for
100  * dynamic loading).
101  */
102 #ifndef WIN32
103  is_r = (access(path, R_OK) == 0);
104  is_x = (access(path, X_OK) == 0);
105 #else
106  is_r = buf.st_mode & S_IRUSR;
107  is_x = buf.st_mode & S_IXUSR;
108 #endif
109  return is_x ? (is_r ? 0 : -2) : -1;
110 }
111 
112 
113 /*
114  * find_my_exec -- find an absolute path to a valid executable
115  *
116  * argv0 is the name passed on the command line
117  * retpath is the output area (must be of size MAXPGPATH)
118  * Returns 0 if OK, -1 if error.
119  *
120  * The reason we have to work so hard to find an absolute path is that
121  * on some platforms we can't do dynamic loading unless we know the
122  * executable's location. Also, we need a full path not a relative
123  * path because we will later change working directory. Finally, we want
124  * a true path not a symlink location, so that we can locate other files
125  * that are part of our installation relative to the executable.
126  */
127 int
128 find_my_exec(const char *argv0, char *retpath)
129 {
130  char cwd[MAXPGPATH],
131  test_path[MAXPGPATH];
132  char *path;
133 
134  if (!getcwd(cwd, MAXPGPATH))
135  {
137  _("could not identify current directory: %m"));
138  return -1;
139  }
140 
141  /*
142  * If argv0 contains a separator, then PATH wasn't used.
143  */
144  if (first_dir_separator(argv0) != NULL)
145  {
146  if (is_absolute_path(argv0))
147  strlcpy(retpath, argv0, MAXPGPATH);
148  else
149  join_path_components(retpath, cwd, argv0);
150  canonicalize_path(retpath);
151 
152  if (validate_exec(retpath) == 0)
153  return resolve_symlinks(retpath);
154 
155  log_error(errcode(ERRCODE_WRONG_OBJECT_TYPE),
156  _("invalid binary \"%s\""), retpath);
157  return -1;
158  }
159 
160 #ifdef WIN32
161  /* Win32 checks the current directory first for names without slashes */
162  join_path_components(retpath, cwd, argv0);
163  if (validate_exec(retpath) == 0)
164  return resolve_symlinks(retpath);
165 #endif
166 
167  /*
168  * Since no explicit path was supplied, the user must have been relying on
169  * PATH. We'll search the same PATH.
170  */
171  if ((path = getenv("PATH")) && *path)
172  {
173  char *startp = NULL,
174  *endp = NULL;
175 
176  do
177  {
178  if (!startp)
179  startp = path;
180  else
181  startp = endp + 1;
182 
183  endp = first_path_var_separator(startp);
184  if (!endp)
185  endp = startp + strlen(startp); /* point to end */
186 
187  strlcpy(test_path, startp, Min(endp - startp + 1, MAXPGPATH));
188 
189  if (is_absolute_path(test_path))
190  join_path_components(retpath, test_path, argv0);
191  else
192  {
193  join_path_components(retpath, cwd, test_path);
194  join_path_components(retpath, retpath, argv0);
195  }
196  canonicalize_path(retpath);
197 
198  switch (validate_exec(retpath))
199  {
200  case 0: /* found ok */
201  return resolve_symlinks(retpath);
202  case -1: /* wasn't even a candidate, keep looking */
203  break;
204  case -2: /* found but disqualified */
205  log_error(errcode(ERRCODE_WRONG_OBJECT_TYPE),
206  _("could not read binary \"%s\""),
207  retpath);
208  break;
209  }
210  } while (*endp);
211  }
212 
213  log_error(errcode(ERRCODE_UNDEFINED_FILE),
214  _("could not find a \"%s\" to execute"), argv0);
215  return -1;
216 }
217 
218 
219 /*
220  * resolve_symlinks - resolve symlinks to the underlying file
221  *
222  * Replace "path" by the absolute path to the referenced file.
223  *
224  * Returns 0 if OK, -1 if error.
225  *
226  * Note: we are not particularly tense about producing nice error messages
227  * because we are not really expecting error here; we just determined that
228  * the symlink does point to a valid executable.
229  */
230 static int
231 resolve_symlinks(char *path)
232 {
233 #ifdef HAVE_READLINK
234  struct stat buf;
235  char orig_wd[MAXPGPATH],
236  link_buf[MAXPGPATH];
237  char *fname;
238 
239  /*
240  * To resolve a symlink properly, we have to chdir into its directory and
241  * then chdir to where the symlink points; otherwise we may fail to
242  * resolve relative links correctly (consider cases involving mount
243  * points, for example). After following the final symlink, we use
244  * getcwd() to figure out where the heck we're at.
245  *
246  * One might think we could skip all this if path doesn't point to a
247  * symlink to start with, but that's wrong. We also want to get rid of
248  * any directory symlinks that are present in the given path. We expect
249  * getcwd() to give us an accurate, symlink-free path.
250  */
251  if (!getcwd(orig_wd, MAXPGPATH))
252  {
254  _("could not identify current directory: %m"));
255  return -1;
256  }
257 
258  for (;;)
259  {
260  char *lsep;
261  int rllen;
262 
263  lsep = last_dir_separator(path);
264  if (lsep)
265  {
266  *lsep = '\0';
267  if (chdir(path) == -1)
268  {
270  _("could not change directory to \"%s\": %m"), path);
271  return -1;
272  }
273  fname = lsep + 1;
274  }
275  else
276  fname = path;
277 
278  if (lstat(fname, &buf) < 0 ||
279  !S_ISLNK(buf.st_mode))
280  break;
281 
282  errno = 0;
283  rllen = readlink(fname, link_buf, sizeof(link_buf));
284  if (rllen < 0 || rllen >= sizeof(link_buf))
285  {
287  _("could not read symbolic link \"%s\": %m"), fname);
288  return -1;
289  }
290  link_buf[rllen] = '\0';
291  strcpy(path, link_buf);
292  }
293 
294  /* must copy final component out of 'path' temporarily */
295  strlcpy(link_buf, fname, sizeof(link_buf));
296 
297  if (!getcwd(path, MAXPGPATH))
298  {
300  _("could not identify current directory: %m"));
301  return -1;
302  }
303  join_path_components(path, path, link_buf);
304  canonicalize_path(path);
305 
306  if (chdir(orig_wd) == -1)
307  {
309  _("could not change directory to \"%s\": %m"), orig_wd);
310  return -1;
311  }
312 #endif /* HAVE_READLINK */
313 
314  return 0;
315 }
316 
317 
318 /*
319  * Find another program in our binary's directory,
320  * then make sure it is the proper version.
321  */
322 int
323 find_other_exec(const char *argv0, const char *target,
324  const char *versionstr, char *retpath)
325 {
326  char cmd[MAXPGPATH];
327  char line[MAXPGPATH];
328 
329  if (find_my_exec(argv0, retpath) < 0)
330  return -1;
331 
332  /* Trim off program name and keep just directory */
333  *last_dir_separator(retpath) = '\0';
334  canonicalize_path(retpath);
335 
336  /* Now append the other program's name */
337  snprintf(retpath + strlen(retpath), MAXPGPATH - strlen(retpath),
338  "/%s%s", target, EXE);
339 
340  if (validate_exec(retpath) != 0)
341  return -1;
342 
343  snprintf(cmd, sizeof(cmd), "\"%s\" -V", retpath);
344 
345  if (!pipe_read_line(cmd, line, sizeof(line)))
346  return -1;
347 
348  if (strcmp(line, versionstr) != 0)
349  return -2;
350 
351  return 0;
352 }
353 
354 
355 /*
356  * Execute a command in a pipe and read the first line from it.
357  */
358 char *
359 pipe_read_line(char *cmd, char *line, int maxsize)
360 {
361  FILE *pgver;
362 
363  /* flush output buffers in case popen does not... */
364  fflush(stdout);
365  fflush(stderr);
366 
367  errno = 0;
368  if ((pgver = popen(cmd, "r")) == NULL)
369  {
370  perror("popen failure");
371  return NULL;
372  }
373 
374  errno = 0;
375  if (fgets(line, maxsize, pgver) == NULL)
376  {
377  if (feof(pgver))
378  fprintf(stderr, "no data was returned by command \"%s\"\n", cmd);
379  else
380  perror("fgets failure");
381  pclose(pgver); /* no error checking */
382  return NULL;
383  }
384 
385  if (pclose_check(pgver))
386  return NULL;
387 
388  return line;
389 }
390 
391 
392 /*
393  * pclose() plus useful error reporting
394  */
395 int
396 pclose_check(FILE *stream)
397 {
398  int exitstatus;
399  char *reason;
400 
401  exitstatus = pclose(stream);
402 
403  if (exitstatus == 0)
404  return 0; /* all is well */
405 
406  if (exitstatus == -1)
407  {
408  /* pclose() itself failed, and hopefully set errno */
409  log_error(errcode(ERRCODE_SYSTEM_ERROR),
410  _("pclose failed: %m"));
411  }
412  else
413  {
414  reason = wait_result_to_str(exitstatus);
415  log_error(errcode(ERRCODE_SYSTEM_ERROR),
416  "%s", reason);
417  pfree(reason);
418  }
419  return exitstatus;
420 }
421 
422 /*
423  * set_pglocale_pgservice
424  *
425  * Set application-specific locale and service directory
426  *
427  * This function takes the value of argv[0] rather than a full path.
428  *
429  * (You may be wondering why this is in exec.c. It requires this module's
430  * services and doesn't introduce any new dependencies, so this seems as
431  * good as anyplace.)
432  */
433 void
434 set_pglocale_pgservice(const char *argv0, const char *app)
435 {
436  char path[MAXPGPATH];
437  char my_exec_path[MAXPGPATH];
438  char env_path[MAXPGPATH + sizeof("PGSYSCONFDIR=")]; /* longer than
439  * PGLOCALEDIR */
440  char *dup_path;
441 
442  /* don't set LC_ALL in the backend */
443  if (strcmp(app, PG_TEXTDOMAIN("postgres")) != 0)
444  {
445  setlocale(LC_ALL, "");
446 
447  /*
448  * One could make a case for reproducing here PostmasterMain()'s test
449  * for whether the process is multithreaded. Unlike the postmaster,
450  * no frontend program calls sigprocmask() or otherwise provides for
451  * mutual exclusion between signal handlers. While frontends using
452  * fork(), if multithreaded, are formally exposed to undefined
453  * behavior, we have not witnessed a concrete bug. Therefore,
454  * complaining about multithreading here may be mere pedantry.
455  */
456  }
457 
458  if (find_my_exec(argv0, my_exec_path) < 0)
459  return;
460 
461 #ifdef ENABLE_NLS
462  get_locale_path(my_exec_path, path);
463  bindtextdomain(app, path);
464  textdomain(app);
465 
466  if (getenv("PGLOCALEDIR") == NULL)
467  {
468  /* set for libpq to use */
469  snprintf(env_path, sizeof(env_path), "PGLOCALEDIR=%s", path);
470  canonicalize_path(env_path + 12);
471  dup_path = strdup(env_path);
472  if (dup_path)
473  putenv(dup_path);
474  }
475 #endif
476 
477  if (getenv("PGSYSCONFDIR") == NULL)
478  {
479  get_etc_path(my_exec_path, path);
480 
481  /* set for libpq to use */
482  snprintf(env_path, sizeof(env_path), "PGSYSCONFDIR=%s", path);
483  canonicalize_path(env_path + 13);
484  dup_path = strdup(env_path);
485  if (dup_path)
486  putenv(dup_path);
487  }
488 }
489 
490 #ifdef WIN32
491 
492 /*
493  * AddUserToTokenDacl(HANDLE hToken)
494  *
495  * This function adds the current user account to the restricted
496  * token used when we create a restricted process.
497  *
498  * This is required because of some security changes in Windows
499  * that appeared in patches to XP/2K3 and in Vista/2008.
500  *
501  * On these machines, the Administrator account is not included in
502  * the default DACL - you just get Administrators + System. For
503  * regular users you get User + System. Because we strip Administrators
504  * when we create the restricted token, we are left with only System
505  * in the DACL which leads to access denied errors for later CreatePipe()
506  * and CreateProcess() calls when running as Administrator.
507  *
508  * This function fixes this problem by modifying the DACL of the
509  * token the process will use, and explicitly re-adding the current
510  * user account. This is still secure because the Administrator account
511  * inherits its privileges from the Administrators group - it doesn't
512  * have any of its own.
513  */
514 BOOL
515 AddUserToTokenDacl(HANDLE hToken)
516 {
517  int i;
518  ACL_SIZE_INFORMATION asi;
519  ACCESS_ALLOWED_ACE *pace;
520  DWORD dwNewAclSize;
521  DWORD dwSize = 0;
522  DWORD dwTokenInfoLength = 0;
523  PACL pacl = NULL;
524  PTOKEN_USER pTokenUser = NULL;
525  TOKEN_DEFAULT_DACL tddNew;
526  TOKEN_DEFAULT_DACL *ptdd = NULL;
527  TOKEN_INFORMATION_CLASS tic = TokenDefaultDacl;
528  BOOL ret = FALSE;
529 
530  /* Figure out the buffer size for the DACL info */
531  if (!GetTokenInformation(hToken, tic, (LPVOID) NULL, dwTokenInfoLength, &dwSize))
532  {
533  if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
534  {
535  ptdd = (TOKEN_DEFAULT_DACL *) LocalAlloc(LPTR, dwSize);
536  if (ptdd == NULL)
537  {
538  log_error(errcode(ERRCODE_OUT_OF_MEMORY),
539  _("out of memory"));
540  goto cleanup;
541  }
542 
543  if (!GetTokenInformation(hToken, tic, (LPVOID) ptdd, dwSize, &dwSize))
544  {
545  log_error(errcode(ERRCODE_SYSTEM_ERROR),
546  "could not get token information: error code %lu",
547  GetLastError());
548  goto cleanup;
549  }
550  }
551  else
552  {
553  log_error(errcode(ERRCODE_SYSTEM_ERROR),
554  "could not get token information buffer size: error code %lu",
555  GetLastError());
556  goto cleanup;
557  }
558  }
559 
560  /* Get the ACL info */
561  if (!GetAclInformation(ptdd->DefaultDacl, (LPVOID) &asi,
562  (DWORD) sizeof(ACL_SIZE_INFORMATION),
563  AclSizeInformation))
564  {
565  log_error(errcode(ERRCODE_SYSTEM_ERROR),
566  "could not get ACL information: error code %lu",
567  GetLastError());
568  goto cleanup;
569  }
570 
571  /* Get the current user SID */
572  if (!GetTokenUser(hToken, &pTokenUser))
573  goto cleanup; /* callee printed a message */
574 
575  /* Figure out the size of the new ACL */
576  dwNewAclSize = asi.AclBytesInUse + sizeof(ACCESS_ALLOWED_ACE) +
577  GetLengthSid(pTokenUser->User.Sid) - sizeof(DWORD);
578 
579  /* Allocate the ACL buffer & initialize it */
580  pacl = (PACL) LocalAlloc(LPTR, dwNewAclSize);
581  if (pacl == NULL)
582  {
583  log_error(errcode(ERRCODE_OUT_OF_MEMORY),
584  _("out of memory"));
585  goto cleanup;
586  }
587 
588  if (!InitializeAcl(pacl, dwNewAclSize, ACL_REVISION))
589  {
590  log_error(errcode(ERRCODE_SYSTEM_ERROR),
591  "could not initialize ACL: error code %lu", GetLastError());
592  goto cleanup;
593  }
594 
595  /* Loop through the existing ACEs, and build the new ACL */
596  for (i = 0; i < (int) asi.AceCount; i++)
597  {
598  if (!GetAce(ptdd->DefaultDacl, i, (LPVOID *) &pace))
599  {
600  log_error(errcode(ERRCODE_SYSTEM_ERROR),
601  "could not get ACE: error code %lu", GetLastError());
602  goto cleanup;
603  }
604 
605  if (!AddAce(pacl, ACL_REVISION, MAXDWORD, pace, ((PACE_HEADER) pace)->AceSize))
606  {
607  log_error(errcode(ERRCODE_SYSTEM_ERROR),
608  "could not add ACE: error code %lu", GetLastError());
609  goto cleanup;
610  }
611  }
612 
613  /* Add the new ACE for the current user */
614  if (!AddAccessAllowedAceEx(pacl, ACL_REVISION, OBJECT_INHERIT_ACE, GENERIC_ALL, pTokenUser->User.Sid))
615  {
616  log_error(errcode(ERRCODE_SYSTEM_ERROR),
617  "could not add access allowed ACE: error code %lu",
618  GetLastError());
619  goto cleanup;
620  }
621 
622  /* Set the new DACL in the token */
623  tddNew.DefaultDacl = pacl;
624 
625  if (!SetTokenInformation(hToken, tic, (LPVOID) &tddNew, dwNewAclSize))
626  {
627  log_error(errcode(ERRCODE_SYSTEM_ERROR),
628  "could not set token information: error code %lu",
629  GetLastError());
630  goto cleanup;
631  }
632 
633  ret = TRUE;
634 
635 cleanup:
636  if (pTokenUser)
637  LocalFree((HLOCAL) pTokenUser);
638 
639  if (pacl)
640  LocalFree((HLOCAL) pacl);
641 
642  if (ptdd)
643  LocalFree((HLOCAL) ptdd);
644 
645  return ret;
646 }
647 
648 /*
649  * GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser)
650  *
651  * Get the users token information from a process token.
652  *
653  * The caller of this function is responsible for calling LocalFree() on the
654  * returned TOKEN_USER memory.
655  */
656 static BOOL
657 GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser)
658 {
659  DWORD dwLength;
660 
661  *ppTokenUser = NULL;
662 
663  if (!GetTokenInformation(hToken,
664  TokenUser,
665  NULL,
666  0,
667  &dwLength))
668  {
669  if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
670  {
671  *ppTokenUser = (PTOKEN_USER) LocalAlloc(LPTR, dwLength);
672 
673  if (*ppTokenUser == NULL)
674  {
675  log_error(errcode(ERRCODE_OUT_OF_MEMORY),
676  _("out of memory"));
677  return FALSE;
678  }
679  }
680  else
681  {
682  log_error(errcode(ERRCODE_SYSTEM_ERROR),
683  "could not get token information buffer size: error code %lu",
684  GetLastError());
685  return FALSE;
686  }
687  }
688 
689  if (!GetTokenInformation(hToken,
690  TokenUser,
691  *ppTokenUser,
692  dwLength,
693  &dwLength))
694  {
695  LocalFree(*ppTokenUser);
696  *ppTokenUser = NULL;
697 
698  log_error(errcode(ERRCODE_SYSTEM_ERROR),
699  "could not get token information: error code %lu",
700  GetLastError());
701  return FALSE;
702  }
703 
704  /* Memory in *ppTokenUser is LocalFree():d by the caller */
705  return TRUE;
706 }
707 
708 #endif
int find_other_exec(const char *argv0, const char *target, const char *versionstr, char *retpath)
Definition: exec.c:323
static char * argv0
Definition: pg_ctl.c:97
static int resolve_symlinks(char *path)
Definition: exec.c:231
#define setlocale(a, b)
Definition: win32_port.h:436
#define Min(x, y)
Definition: c.h:982
void canonicalize_path(char *path)
Definition: path.c:254
int errcode(int sqlerrcode)
Definition: elog.c:691
int pg_strcasecmp(const char *s1, const char *s2)
Definition: pgstrcasecmp.c:36
BOOL AddUserToTokenDacl(HANDLE hToken)
#define S_IXUSR
Definition: win32_port.h:286
#define fprintf
Definition: port.h:219
char * wait_result_to_str(int exitstatus)
Definition: wait_error.c:32
int pclose_check(FILE *stream)
Definition: exec.c:396
void pfree(void *pointer)
Definition: mcxt.c:1057
#define log_error(errcodefn,...)
Definition: exec.c:41
#define MAXPGPATH
static int validate_exec(const char *path)
Definition: exec.c:67
int find_my_exec(const char *argv0, char *retpath)
Definition: exec.c:128
#define readlink(path, buf, size)
Definition: win32_port.h:228
int errcode_for_file_access(void)
Definition: elog.c:714
#define is_absolute_path(filename)
Definition: port.h:86
#define S_ISREG(m)
Definition: win32_port.h:319
char my_exec_path[MAXPGPATH]
Definition: globals.c:72
#define PG_TEXTDOMAIN(domain)
Definition: c.h:1211
char * first_dir_separator(const char *filename)
Definition: path.c:103
unsigned short st_mode
Definition: win32_port.h:260
static void cleanup(void)
Definition: bootstrap.c:862
char * last_dir_separator(const char *filename)
Definition: path.c:138
void get_etc_path(const char *my_exec_path, char *ret_path)
Definition: path.c:713
size_t strlcpy(char *dst, const char *src, size_t siz)
Definition: strlcpy.c:45
#define putenv(x)
Definition: win32_port.h:502
void join_path_components(char *ret_path, const char *head, const char *tail)
Definition: path.c:218
#define S_IRUSR
Definition: win32_port.h:280
#define lstat(path, sb)
Definition: win32_port.h:276
void set_pglocale_pgservice(const char *argv0, const char *app)
Definition: exec.c:434
int i
char * pipe_read_line(char *cmd, char *line, int maxsize)
Definition: exec.c:359
#define EXE
Definition: port.h:140
void get_locale_path(const char *my_exec_path, char *ret_path)
Definition: path.c:767
#define snprintf
Definition: port.h:215
#define _(x)
Definition: elog.c:88
char * first_path_var_separator(const char *pathlist)
Definition: path.c:120
#define stat
Definition: win32_port.h:275