Recursively list directories
#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)


There's a long-standing convention to use UPPER_CASE for preprocessor macros.  This helps draw attention to them as text replacements, which don't obey the same rules of scope as C identifiers (and which sometimes - though not in this case - expand their arguments multiple times).

void print_files(char* dir, FILE* out)


Why a const char* dir?  Surely we have no need to overwrite its contents?  Also, I'd be inclined to always write to stdout in this program - let the user apply ordinary shell plumbing if a different destination is desired.  That's easier than having a special argument in each program.
In larger programs, it's usually a bad idea to exit() from within functions, particularly if it's possible to either recover, or produce useful information regardless of the error.  As a concrete example, if we find a subdirectory we can't read, it's probably more helpful to emit a warning message to stderr but continue listing the directories the user does have permissions for.
The code using open() and syscall() is much lower level than we'd normally use for this, and tightly bound to Linux rather than POSIX as claimed.  I think opendir() and readdir() would be more suited.
Be careful with resource management and non-local exits in C.  We only reach close(fd) in the success case.  Although handle_error() currently exits the program, if we change that in future, we'll find ourselves leaking file descriptors.  Kudos for closing the descriptor before the recursive call - it's easy to leave it open until the end of the function, and you avoided that trap.

char* buffer = malloc(sizeof *buffer * BUF_SIZE);


The multiplication by sizeof *buffer is borderline pointless.  I say &quot;borderline&quot; in case there's a chance that we might want to change the type (but still have BUF_SIZE of them).
We don't seem to handle reading directories larger than BUF_SIZE at all - not even with an error message.

    char path[400];
    strcpy(path, dir);
    strcat(path, &quot;/&quot;);
    strcat(path, d-&gt;d_name);


Where's that 400 come from?  Why not use the system's MAX_PATH constant?
strcpy() followed by strcat() is wasteful - that causes another traversal of the string just written, in order to find where to start writing.  In this case, we could use printf() like this:
int path_len = snprintf(path, sizeof path, &quot;%s/%s&quot;, dir, d-&gt;d_name);

Remember to test path_len to determine whether the path was truncated at this stage.

    if (d_type == DT_DIR)
         print_files(path, out);
    else if (d_type == DT_REG)
        fprintf(out, &quot;%s\n&quot;, path);


Why don't we print the other kinds of file?  I wouldn't expect all my symlinks, sockets, FIFOs and device files to simply disappear from the listing unless I specifically asked for that.

   fprintf(out, &quot;-------------[ START ]---------------------\n&quot;);


Consider using fputs() when there's no conversions to be done.
As a general style point, I'd advise more vertical space for conditionals, and use of braces as a consistent rule.  E.g.
if (buffer == NULL) {
    handle_error(&quot;Error malloc.\n&quot;);
}