4

I need to use syscall internally in Android NDK to prevent hooking of the wrapper functions. In Linux there are macros like SYSCALL_INLINE which allows using syscall without wrapper function. Thus the macro embeds the syscall assembly code into the project directly.

I could not find similar macro in Android NDK.

Maybe I can write my own functions like this one; https://git.busybox.net/uClibc/tree/libc/sysdeps/linux/arm/syscall.c

But I need to have arm, arm_64, x86 and x86_64 versions of the same function.

Can you help me? How can I find a solution?

Phillip
  • 259
  • 1
  • 2
  • 11
  • 1
    What kind of threat do you want to protect from? Usually on Android, your app runs in a separate sandbox, and no untrusted agent can hook the syscall wrapper functions. – Alex Cohn May 29 '18 at 10:42
  • I can imagine this one (http://frida.re/) or this one (https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) or even this one (https://linux.die.net/man/1/ltrace) @AlexCohn – solidak May 20 '19 at 07:07

1 Answers1

6

Android's Linux kernel still uses the same system-call numbers and ABI as regular Linux, doesn't it? (So How to access the system call from user-space?) So you should be able to use the normal methods, with call numbers from <asm/unistd.h>.

You could use the MUSL libc syscall inline functions in arch/x86_64/syscall_arch.h. It has different ones for each different number of args, instead of one big one.


MUSL has versions of syscall_arch.h for ARM, AArch64, i386, and x86-64, as well as other architectures it supports. It's licensed under a permissive MIT license, so you can just copy those headers.

For example, their ARM version defines __asm_syscall as a macro that (in non-Thumb mode) does asm volatile("svc 0" : "=r"(r0) : inputs); return r0;, and defines wrappers like:

static inline long __syscall3(long n, long a, long b, long c)
{
    register long r7 __ASM____R7__ = n;  // macro trickery for not clobbering r7 in thumb mode (where it may be the frame pointer)
    register long r0 __asm__("r0") = a;
    register long r1 __asm__("r1") = b;
    register long r2 __asm__("r2") = c;
    __asm_syscall(R7_OPERAND, "0"(r0), "r"(r1), "r"(r2));
    // includes "=r0"(r0) and return r0
 // FIXME: add a "memory" clobber because pointed-to memory can be an input or output
}

Unfortunately this is not safe: this doesn't tell the compiler that pointer operands are dereferenced, so it might treat stores into a buffer before write() as dead stores and optimize them away!

This is trivial to fix: add a "memory" clobber. Or see How can I indicate that the memory *pointed* to by an inline ASM argument may be used? for ways to just tell the compiler about one operand, for a specific system call where you know which operands are pointers and whether they're inputs, outputs, or both.

IDK if that was part of glibc's motivation for removing its similar syscall macros and only providing a non-inline syscall function. Or maybe they didn't want to encourage people to embed the system-call ABI into their program so it could in theory change to become more efficient in the future.

You'd use it like

#include <asm/unistd.h>   // for __NR_write
#include <stdlib.h>       // for ssize_t
#include "syscall_arch.h"

// doesn't set errno or force all error returns to -1
// return values from -1 to -4095 are errors, e.g. -EBADF or -EFAULT

__attribte__((noinline))  // hack for inline asm unsafety
ssize_t my_write(int fd, const void *buf, size_t count) {
    return __syscall3(__NR_write, fd, (long)buf, count);
}

I put this on the Godbolt compiler explorer with enough of ARM syscall_arch.h copied in to make this compile. Some of Godbolt's ARM gcc installs have missing <asm/unistd.h>, but gcc5.4 has a working one. The result in ARM mode is:

my_write:
    str     r7, [sp, #-4]!
    mov     r7, #4
@ system-calling convention mostly matches function-calling convention
@ so args are in the right registers already
    svc 0
    ldr     r7, [sp], #4
    bx      lr

And of course this function can inline into a caller so the save/restore of r7 happens once for the whole function.

(edit): this would be unsafe if inlined into a caller where dead stores could optimize away. A better brute-force option would be a memory clobber on the inline asm statement, or more work would be to add a dummy memory operand for system calls that read or write user-space memory (see at&t asm inline c++ problem). Or for munmap to make sure no stores into the page(s) being freed sink past it and happen after the memory is unmapped.

Even without inlining, it's inter-procedural optimization makes this not strictly safe, so __attribute__((noinline,noipa)), or just use a "memory" clobber in the asm statement!

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • You are legend! Very good answer. What are the __SYSCALL_LL_E and __SYSCALL_LL_O macros? Do I need to care about __LP64__ or not? – Phillip May 30 '18 at 08:22
  • @Phillip: AFAIK, the header files Just Work. I haven't looked at the AArch64 one, I assume that's where you found `__LP64`? That would be set for an ABI where `long` and pointers are 64-bit, or not defined for an ILP32 model (32-bit pointers in 64-bit mode). You only need to worry about it if Android NDK doesn't define it when needed. – Peter Cordes May 30 '18 at 09:13
  • Thank you very much, you are the best. I see that LP64 is already defined in android NDK headers. – Phillip May 30 '18 at 10:16
  • I'm getting this error for i386 `function my_write(int, void const*, unsigned int): error: undefined reference to '__vsyscall'` I have put #if __i386__ directive to disable this code for other cpu-s Do you have any idea? – Phillip May 30 '18 at 13:03
  • Oh, I didn't look at what MUSL did for i386. But yeah, on 32-bit x86 the best way to make system calls is to `call` into the page of code that the kernel maps into the virtual memory of user-space processes (with ELF shared library metadata). See https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/ for more info about the VDSO, in case you need to do something to make it link on i386. But why are you including the i386 definitions on other architectures at all? **You want to include exactly one version of the header, the one for the target architecture.** – Peter Cordes May 30 '18 at 13:30
  • I include the header files under the `#if __arm__`, `#if __aarch64__`, `#if __i386__`, `#if __x86_64__` preprocessors to select correct one for the current architecture. My app supports 4 platforms now (arm, arm_64, x86, x86_64). But the x86 one generates this error. I don't want to link any other library, can't I use an assembly code instead of `__vsyscall` or other solution? – Phillip May 30 '18 at 14:47
  • @Phillip: did you read that link? It isn't a library on disk, it's user-space code provided directly by the kernel. But yes, you could use the slower `int 0x80` 32-bit ABI. – Peter Cordes May 30 '18 at 14:55
  • Hello @Peter I still could not figure out the C++ issue, I have replaced the `(int){0}` with a tmp variable but should I do something further with the tmp variable? I have added my latest macro code in chat. – Phillip Jun 04 '18 at 09:13