void pointer as argument

Passing a pointer to a1 to your function, you can't change where a1 points. The pointer is passed by value, so in f1 you're only changing a copy of the address held by a. If you want to change the pointer, i.e. allocate new memory for the pointer passed in, then you'll need to pass a pointer to a pointer:

void f1(void **a)
{
    // ...
    *a = malloc(sizeof(int));
    // ...

As this is C, you cannot pass the pointer by reference without passing in a pointer to the pointer (e.g., void ** rather than void * to point to the pointer). You need to return the new pointer. What is happening:

f(a1);

Pushes the value of the pointer (NULL) as the stack parameter value for a. a picks up this value, and then reassigns itself a new value (the malloced address). As it was passed by value, nothing changes for a1.

If this were C++, you could achieve what you wanted by doing passing the pointer by reference:

void f(void *&a);

To change a variable via a function call, the function needs to have reference semantics with respect to the argument. C doesn't have native reference variables, but can implement reference semantics by means of taking addresses and passing pointers.

Generally:

void mutate_thing(Thing * x)    // callee accepts pointer
{
    *x = stuff;                 // callee derefences ("*")
}

int main()
{
    Thing y;
    mutate_thing(&y);           // caller takes address-of ("&")
}

In your case, the Thing is void *:

void f(void ** pv)
{
    *pv = malloc(12);   // or whatever
}

int main()
{
     void * a1;
     f(&a1);
}