Initial stack construction

I'm having problems constructing the initial stack for the guest executable for Valgrind on macOS 12 Intel. This seemed to work OK for macOS 11 but I'm getting a bad 'apple' pointer on macOS 12.

The stack (constructed by Valgrind) looks like this

   higher address +-----------------+ <- clstack_end
                  |                 |
                  : string table    :
                  |                 |
                  +-----------------+
                  | NULL            |
                  +-----------------+
                  | executable_path | (first arg to execve())
                  +-----------------+
                  | NULL            |
                  -                 -
                  | envp            |
                  +-----------------+
                  | NULL            |
                  -                 -
                  | argv            |
                  +-----------------+
                  | argc            |
                  +-----------------+
                  | mach_header *   | (dynamic only)
   lower address  +-----------------+ <- sp
                  | undefined       |
                  :                 :

The problem that I'm having is with the executable path (or the apple pointer). This points to NULL. The actual pointer to the "executable=xxx" string is 16 bytes lower in memory.

The code for main starts with

Dump of assembler code for function main:
   0x0000000100003a90 <+0>:     push   %rbp
   0x0000000100003a91 <+1>:     mov    %rsp,%rbp
   0x0000000100003a94 <+4>:     sub    $0x60,%rsp
   0x0000000100003a98 <+8>:     movl   $0x0,-0x4(%rbp)
   0x0000000100003a9f <+15>:    mov    %edi,-0x8(%rbp)
   0x0000000100003aa2 <+18>:    mov    %rsi,-0x10(%rbp)
   0x0000000100003aa6 <+22>:    mov    %rdx,-0x18(%rbp)
   0x0000000100003aaa <+26>:    mov    %rcx,-0x20(%rbp)

That's the prefix, making space for locals, setting a local variable to 0 then getting the 4 arguments from main in edi, rsi, rdx and rcx as per the SYSV amd64 ABI.

I think that it is dyld that puts the apple pointer into rcx.

Can anyone tall me how dyld works out the address of the apple pointer?

It looks to me like this is the dyld start code

https://github.com/apple-opensource/dyld/blob/e3f88907bebb8421f50f0943595f6874de70ebe0/src/dyldInitialization.cpp#L130

As one might expect it just walks over the env pointers plus one NULL to get to the apple pointer.

It looks like you figured this out for yourself. Cool.

IMPORTANT None of this is considered API. Usually I warn folks about relying on such implementation details, but it’s hard to avoid given your overall goal.

Oh, and one fun thing I learnt recently is that you can access the apple pointer as an extra argument on main:

int main(int argc, char **argv, char **env, char **apple) {
    …
}

Of course, that’s the main called by dyld, not the main called by the kernel.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I've done a lot more work on this, and I have the impression that this is a bug in macOS, at least on 12 and 13 Intel.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/syslimits.h>


// used for debugging apple pointer issues, see
// https://bugs.kde.org/show_bug.cgi?id=517304
#define DEBUG_ENV

// On Darwin there's this secret fourth argument, 'apple'.
// That's kind of like a cut down obfuscated version of auxv.
// For the moment we only support the first entry, executable_path=

int main(int argc, char *argv[], char *envp[], char *apple[])
{
   char *pargv = calloc((PATH_MAX+1), sizeof(char)),
        *pappl = calloc((PATH_MAX+1), sizeof(char));
   int i;

   for (i = 0; envp[i]; i++) {
#if defined(DEBUG_ENV)
      fprintf(stderr, "apple-main-arg: i %d &envp[i] %p envp[i] %s\n", i, &envp[i], envp[i]);
#endif
   }

#if defined(DEBUG_ENV)
   fprintf(stderr, "2 slots after envp\n");
   fprintf(stderr, "apple-main-arg: i %d &envp[i] %p envp[i] %s\n", i, &envp[i], envp[i]);
   fprintf(stderr, "apple-main-arg: i %d &envp[i] %p envp[i] %s\n", i+1, &envp[i+1], envp[i+1]);
   fprintf(stderr, "apple-main-arg: i %d &envp[i] %p envp[i] %s\n", i+2, &envp[i+2], envp[i+2]);
   fprintf(stderr, "apple %p\n", apple);
   int j = 0;
   while (apple[j]) {
      fprintf(stderr, "apple-main-arg: j %d &apple[j] %p apple[j] %s\n", j, &apple[j], apple[j]);
      ++j;
   }
   if (j == 0) {
      fprintf(stderr, "apple-main-arg: j %d &apple[j] %p apple[j] %s\n", j, &apple[j], apple[j]);
      fprintf(stderr, "apple-main-arg: 1 slot after apple\n");
      fprintf(stderr, "apple-main-arg: j %d &apple[j] %p apple[j] %s\n", j+1, &apple[j+1], apple[j+1]);
   } else {
      fprintf(stderr, "apple-main-arg: 1 slot after apple\n");
      fprintf(stderr, "apple-main-arg: j %d &apple[j] %p apple[j] %s\n", j, &apple[j], apple[j]);
   }
#endif

   // envp[i]==NULL; envp[i+1]==apple[0]==executable_path
   assert(envp[i+1] == apple[0]);

   // Make sure realpath(argv[0]) == realpath(apple[0]).  (realpath resolves
   // symlinks.)
   // PJF this changed with macOS 10.14, apple path now has a prefix
   const char prefix[] = "executable_path=";
   const size_t prefix_len = strlen(prefix);
   assert(strncmp(apple[0], prefix, prefix_len) == 0);
   realpath(apple[0]+prefix_len, pappl);
   realpath(argv[0], pargv);
   assert(0 == strcmp(pargv, pappl));

   free(pargv);
   free(pappl);

   return 0;
}

If I build and run the above then, on its own it will run OK.

If I run it with

export DYLD_INSERT_LIBRARIES=/Users/paulf/scratch/valgrind/none/tests/darwin/../../../.in_place/vgpreload_core-amd64-darwin.so

then it fails

apple-main-arg: i 24 &envp[i] 0x7ff7b1a48af8 envp[i] A__z="*SHLVL
2 slots after envp
apple-main-arg: i 25 &envp[i] 0x7ff7b1a48b00 envp[i] (null)
apple-main-arg: i 26 &envp[i] 0x7ff7b1a48b08 envp[i] executable_path=./apple-main-arg
apple-main-arg: i 27 &envp[i] 0x7ff7b1a48b10 envp[i] (null)
apple 0x7ff7b1a48b10
apple-main-arg: j 0 &apple[j] 0x7ff7b1a48b10 apple[j] (null)
apple-main-arg: 1 slot after apple
apple-main-arg: j 1 &apple[j] 0x7ff7b1a48b18 apple[j] (null)
Assertion failed: (envp[i+1] == apple[0]), function main, file apple-main-arg.c, line 51.
./test.ksh: line 7: 3524: Abort
Abort
at least on 12 and 13 Intel.

Does that mean you only tested in on macOS 12 and 13? Or that you tested it on newer systems and things were better there?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

OK I found the problem and it's on the Valgrind side. A case of the left hand not knowing what the right hand is doing.

This is the code that causes the problem

static void vg_cleanup_env(void)  __attribute__((constructor));
static void vg_cleanup_env(void)
{
    HChar **envp = (HChar**)*_NSGetEnviron();
    env_unsetenv(envp, "VALGRIND_LAUNCHER");
    env_unsetenv(envp, "DYLD_SHARED_REGION");
    // GrP fixme should be more like mash_colon_env()
    env_unsetenv(envp, "DYLD_INSERT_LIBRARIES");
}

'env_unsetenv' just moves down pointers in the envp table (and the apple param pointers). But it does this after dyld has put the apple pointer into the arguments for main.

The fix (from Louis Bruner's port) is to not modify the existing apple pointer table. The first element gets duplicated.

So we end up with something like

            +-----------------+ 
            |                 |
            :  string table   :
            |                 |
    	       +-----------------+
	          |      NULL        |
            +-----------------+
            | executable_path  | 
            | executable_path  | 
            | executable_path  | 
            +-----------------+
            |      NULL        |
            -                  -
            |       envp       |
Initial stack construction
 
 
Q