Coverage Report

Created: 2026-08-14 06:37

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