Skip to content

Commit 11a0a8b

Browse files
committed
Implement find_my_exec()'s path normalization using realpath(3).
Replace the symlink-chasing logic in find_my_exec with realpath(3), which has been required by POSIX since SUSv2. (Windows lacks realpath(), but there we can use _fullpath() which is functionally equivalent.) The main benefit of this is that -- on all modern platforms at least -- realpath() avoids the chdir() shenanigans we used to perform while interpreting symlinks. That had various corner-case failure modes so it's good to get rid of it. There is still ongoing discussion about whether we could skip the replacement of symlinks in some cases, but that's really matter for a separate patch. Meanwhile I want to push this before we get too close to feature freeze, so that we can find out if there are showstopper portability issues. Discussion: https://postgr.es/m/797232.1662075573@sss.pgh.pa.us
1 parent eb2618a commit 11a0a8b

File tree

1 file changed

+91
-115
lines changed

1 file changed

+91
-115
lines changed

src/common/exec.c

+91-115
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
*-------------------------------------------------------------------------
1515
*/
1616

17+
/*
18+
* On macOS, "man realpath" avers:
19+
* Defining _DARWIN_C_SOURCE or _DARWIN_BETTER_REALPATH before including
20+
* stdlib.h will cause the provided implementation of realpath() to use
21+
* F_GETPATH from fcntl(2) to discover the path.
22+
* This should be harmless everywhere else.
23+
*/
24+
#define _DARWIN_BETTER_REALPATH
25+
1726
#ifndef FRONTEND
1827
#include "postgres.h"
1928
#else
@@ -58,11 +67,8 @@ extern int _CRT_glob = 0; /* 0 turns off globbing; 1 turns it on */
5867
(fprintf(stderr, __VA_ARGS__), fputc('\n', stderr))
5968
#endif
6069

61-
#ifdef _MSC_VER
62-
#define getcwd(cwd,len) GetCurrentDirectory(len, cwd)
63-
#endif
64-
65-
static int resolve_symlinks(char *path);
70+
static int normalize_exec_path(char *path);
71+
static char *pg_realpath(const char *fname);
6672

6773
#ifdef WIN32
6874
static BOOL GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser);
@@ -87,7 +93,7 @@ validate_exec(const char *path)
8793
char path_exe[MAXPGPATH + sizeof(".exe") - 1];
8894

8995
/* Win32 requires a .exe suffix for stat() */
90-
if (strlen(path) >= strlen(".exe") &&
96+
if (strlen(path) < strlen(".exe") ||
9197
pg_strcasecmp(path + strlen(path) - strlen(".exe"), ".exe") != 0)
9298
{
9399
strlcpy(path_exe, path, sizeof(path_exe) - 4);
@@ -135,46 +141,32 @@ validate_exec(const char *path)
135141

136142

137143
/*
138-
* find_my_exec -- find an absolute path to a valid executable
144+
* find_my_exec -- find an absolute path to this program's executable
139145
*
140146
* argv0 is the name passed on the command line
141147
* retpath is the output area (must be of size MAXPGPATH)
142148
* Returns 0 if OK, -1 if error.
143149
*
144150
* The reason we have to work so hard to find an absolute path is that
145151
* on some platforms we can't do dynamic loading unless we know the
146-
* executable's location. Also, we need a full path not a relative
147-
* path because we will later change working directory. Finally, we want
152+
* executable's location. Also, we need an absolute path not a relative
153+
* path because we may later change working directory. Finally, we want
148154
* a true path not a symlink location, so that we can locate other files
149155
* that are part of our installation relative to the executable.
150156
*/
151157
int
152158
find_my_exec(const char *argv0, char *retpath)
153159
{
154-
char cwd[MAXPGPATH],
155-
test_path[MAXPGPATH];
156160
char *path;
157161

158-
if (!getcwd(cwd, MAXPGPATH))
159-
{
160-
log_error(errcode_for_file_access(),
161-
_("could not identify current directory: %m"));
162-
return -1;
163-
}
164-
165162
/*
166163
* If argv0 contains a separator, then PATH wasn't used.
167164
*/
168-
if (first_dir_separator(argv0) != NULL)
165+
strlcpy(retpath, argv0, MAXPGPATH);
166+
if (first_dir_separator(retpath) != NULL)
169167
{
170-
if (is_absolute_path(argv0))
171-
strlcpy(retpath, argv0, MAXPGPATH);
172-
else
173-
join_path_components(retpath, cwd, argv0);
174-
canonicalize_path(retpath);
175-
176168
if (validate_exec(retpath) == 0)
177-
return resolve_symlinks(retpath);
169+
return normalize_exec_path(retpath);
178170

179171
log_error(errcode(ERRCODE_WRONG_OBJECT_TYPE),
180172
_("invalid binary \"%s\": %m"), retpath);
@@ -183,9 +175,8 @@ find_my_exec(const char *argv0, char *retpath)
183175

184176
#ifdef WIN32
185177
/* Win32 checks the current directory first for names without slashes */
186-
join_path_components(retpath, cwd, argv0);
187178
if (validate_exec(retpath) == 0)
188-
return resolve_symlinks(retpath);
179+
return normalize_exec_path(retpath);
189180
#endif
190181

191182
/*
@@ -208,21 +199,15 @@ find_my_exec(const char *argv0, char *retpath)
208199
if (!endp)
209200
endp = startp + strlen(startp); /* point to end */
210201

211-
strlcpy(test_path, startp, Min(endp - startp + 1, MAXPGPATH));
202+
strlcpy(retpath, startp, Min(endp - startp + 1, MAXPGPATH));
212203

213-
if (is_absolute_path(test_path))
214-
join_path_components(retpath, test_path, argv0);
215-
else
216-
{
217-
join_path_components(retpath, cwd, test_path);
218-
join_path_components(retpath, retpath, argv0);
219-
}
204+
join_path_components(retpath, retpath, argv0);
220205
canonicalize_path(retpath);
221206

222207
switch (validate_exec(retpath))
223208
{
224209
case 0: /* found ok */
225-
return resolve_symlinks(retpath);
210+
return normalize_exec_path(retpath);
226211
case -1: /* wasn't even a candidate, keep looking */
227212
break;
228213
case -2: /* found but disqualified */
@@ -241,105 +226,96 @@ find_my_exec(const char *argv0, char *retpath)
241226

242227

243228
/*
244-
* resolve_symlinks - resolve symlinks to the underlying file
229+
* normalize_exec_path - resolve symlinks and convert to absolute path
245230
*
246-
* Replace "path" by the absolute path to the referenced file.
231+
* Given a path that refers to an executable, chase through any symlinks
232+
* to find the real file location; then convert that to an absolute path.
247233
*
234+
* On success, replaces the contents of "path" with the absolute path.
235+
* ("path" is assumed to be of size MAXPGPATH.)
248236
* Returns 0 if OK, -1 if error.
249-
*
250-
* Note: we are not particularly tense about producing nice error messages
251-
* because we are not really expecting error here; we just determined that
252-
* the symlink does point to a valid executable.
253-
*
254-
* Here we test HAVE_READLINK, which excludes Windows. There's no point in
255-
* using our junction point-based replacement code for this, because that only
256-
* works for directories.
257237
*/
258238
static int
259-
resolve_symlinks(char *path)
239+
normalize_exec_path(char *path)
260240
{
261-
#ifdef HAVE_READLINK
262-
struct stat buf;
263-
char orig_wd[MAXPGPATH],
264-
link_buf[MAXPGPATH];
265-
char *fname;
266-
267241
/*
268-
* To resolve a symlink properly, we have to chdir into its directory and
269-
* then chdir to where the symlink points; otherwise we may fail to
270-
* resolve relative links correctly (consider cases involving mount
271-
* points, for example). After following the final symlink, we use
272-
* getcwd() to figure out where the heck we're at.
273-
*
274-
* One might think we could skip all this if path doesn't point to a
275-
* symlink to start with, but that's wrong. We also want to get rid of
276-
* any directory symlinks that are present in the given path. We expect
277-
* getcwd() to give us an accurate, symlink-free path.
242+
* We used to do a lot of work ourselves here, but now we just let
243+
* realpath(3) do all the heavy lifting.
278244
*/
279-
if (!getcwd(orig_wd, MAXPGPATH))
245+
char *abspath = pg_realpath(path);
246+
247+
if (abspath == NULL)
280248
{
281249
log_error(errcode_for_file_access(),
282-
_("could not identify current directory: %m"));
250+
_("could not resolve path \"%s\" to absolute form: %m"),
251+
path);
283252
return -1;
284253
}
254+
strlcpy(path, abspath, MAXPGPATH);
255+
free(abspath);
285256

286-
for (;;)
287-
{
288-
char *lsep;
289-
int rllen;
290-
291-
lsep = last_dir_separator(path);
292-
if (lsep)
293-
{
294-
*lsep = '\0';
295-
if (chdir(path) == -1)
296-
{
297-
log_error(errcode_for_file_access(),
298-
_("could not change directory to \"%s\": %m"), path);
299-
return -1;
300-
}
301-
fname = lsep + 1;
302-
}
303-
else
304-
fname = path;
257+
#ifdef WIN32
258+
/* On Windows, be sure to convert '\' to '/' */
259+
canonicalize_path(path);
260+
#endif
305261

306-
if (lstat(fname, &buf) < 0 ||
307-
!S_ISLNK(buf.st_mode))
308-
break;
262+
return 0;
263+
}
309264

310-
errno = 0;
311-
rllen = readlink(fname, link_buf, sizeof(link_buf));
312-
if (rllen < 0 || rllen >= sizeof(link_buf))
313-
{
314-
log_error(errcode_for_file_access(),
315-
_("could not read symbolic link \"%s\": %m"), fname);
316-
return -1;
317-
}
318-
link_buf[rllen] = '\0';
319-
strcpy(path, link_buf);
320-
}
321265

322-
/* must copy final component out of 'path' temporarily */
323-
strlcpy(link_buf, fname, sizeof(link_buf));
266+
/*
267+
* pg_realpath() - realpath(3) with POSIX.1-2008 semantics
268+
*
269+
* This is equivalent to realpath(fname, NULL), in that it returns a
270+
* malloc'd buffer containing the absolute path equivalent to fname.
271+
* On error, returns NULL with errno set.
272+
*
273+
* On Windows, what you get is spelled per platform conventions,
274+
* so you probably want to apply canonicalize_path() to the result.
275+
*
276+
* For now, this is needed only here so mark it static. If you choose to
277+
* move it into its own file, move the _DARWIN_BETTER_REALPATH #define too!
278+
*/
279+
static char *
280+
pg_realpath(const char *fname)
281+
{
282+
char *path;
324283

325-
if (!getcwd(path, MAXPGPATH))
284+
#ifndef WIN32
285+
path = realpath(fname, NULL);
286+
if (path == NULL && errno == EINVAL)
326287
{
327-
log_error(errcode_for_file_access(),
328-
_("could not identify current directory: %m"));
329-
return -1;
330-
}
331-
join_path_components(path, path, link_buf);
332-
canonicalize_path(path);
288+
/*
289+
* Cope with old-POSIX systems that require a user-provided buffer.
290+
* Assume MAXPGPATH is enough room on all such systems.
291+
*/
292+
char *buf = malloc(MAXPGPATH);
333293

334-
if (chdir(orig_wd) == -1)
335-
{
336-
log_error(errcode_for_file_access(),
337-
_("could not change directory to \"%s\": %m"), orig_wd);
338-
return -1;
294+
if (buf == NULL)
295+
return NULL; /* assume errno is set */
296+
path = realpath(fname, buf);
297+
if (path == NULL) /* don't leak memory */
298+
{
299+
int save_errno = errno;
300+
301+
free(buf);
302+
errno = save_errno;
303+
}
339304
}
340-
#endif /* HAVE_READLINK */
305+
#else /* WIN32 */
341306

342-
return 0;
307+
/*
308+
* Microsoft is resolutely non-POSIX, but _fullpath() does the same thing.
309+
* The documentation claims it reports errors by setting errno, which is a
310+
* bit surprising for Microsoft, but we'll believe that until it's proven
311+
* wrong. Clear errno first, though, so we can at least tell if a failure
312+
* occurs and doesn't set it.
313+
*/
314+
errno = 0;
315+
path = _fullpath(NULL, fname, 0);
316+
#endif
317+
318+
return path;
343319
}
344320

345321

0 commit comments

Comments
 (0)