/* * Syscall implementations for semihosting. * * Copyright (c) 2022 Linaro * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "qemu/osdep.h" #include "exec/gdbstub.h" #include "semihosting/guestfd.h" #include "semihosting/syscalls.h" #ifdef CONFIG_USER_ONLY #include "qemu.h" #else #include "semihosting/softmmu-uaccess.h" #endif /* * Validate or compute the length of the string (including terminator). */ static int validate_strlen(CPUState *cs, target_ulong str, target_ulong tlen) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char c; if (tlen == 0) { ssize_t slen = target_strlen(str); if (slen < 0) { return -EFAULT; } if (slen >= INT32_MAX) { return -ENAMETOOLONG; } return slen + 1; } if (tlen > INT32_MAX) { return -ENAMETOOLONG; } if (get_user_u8(c, str + tlen - 1)) { return -EFAULT; } if (c != 0) { return -EINVAL; } return tlen; } static int validate_lock_user_string(char **pstr, CPUState *cs, target_ulong tstr, target_ulong tlen) { int ret = validate_strlen(cs, tstr, tlen); CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char *str = NULL; if (ret > 0) { str = lock_user(VERIFY_READ, tstr, ret, true); ret = str ? 0 : -EFAULT; } *pstr = str; return ret; } /* * TODO: Note that gdb always stores the stat structure big-endian. * So far, that's ok, as the only two targets using this are also * big-endian. Until we do something with gdb, also produce the * same big-endian result from the host. */ static int copy_stat_to_user(CPUState *cs, target_ulong addr, const struct stat *s) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; struct gdb_stat *p; if (s->st_dev != (uint32_t)s->st_dev || s->st_ino != (uint32_t)s->st_ino) { return -EOVERFLOW; } p = lock_user(VERIFY_WRITE, addr, sizeof(struct gdb_stat), 0); if (!p) { return -EFAULT; } p->gdb_st_dev = cpu_to_be32(s->st_dev); p->gdb_st_ino = cpu_to_be32(s->st_ino); p->gdb_st_mode = cpu_to_be32(s->st_mode); p->gdb_st_nlink = cpu_to_be32(s->st_nlink); p->gdb_st_uid = cpu_to_be32(s->st_uid); p->gdb_st_gid = cpu_to_be32(s->st_gid); p->gdb_st_rdev = cpu_to_be32(s->st_rdev); p->gdb_st_size = cpu_to_be64(s->st_size); #ifdef _WIN32 /* Windows stat is missing some fields. */ p->gdb_st_blksize = 0; p->gdb_st_blocks = 0; #else p->gdb_st_blksize = cpu_to_be64(s->st_blksize); p->gdb_st_blocks = cpu_to_be64(s->st_blocks); #endif p->gdb_st_atime = cpu_to_be32(s->st_atime); p->gdb_st_mtime = cpu_to_be32(s->st_mtime); p->gdb_st_ctime = cpu_to_be32(s->st_ctime); unlock_user(p, addr, sizeof(struct gdb_stat)); return 0; } /* * GDB semihosting syscall implementations. */ static gdb_syscall_complete_cb gdb_open_complete; static void gdb_open_cb(CPUState *cs, uint64_t ret, int err) { if (!err) { int guestfd = alloc_guestfd(); associate_guestfd(guestfd, ret); ret = guestfd; } gdb_open_complete(cs, ret, err); } static void gdb_open(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, int gdb_flags, int mode) { int len = validate_strlen(cs, fname, fname_len); if (len < 0) { complete(cs, -1, -len); return; } gdb_open_complete = complete; gdb_do_syscall(gdb_open_cb, "open,%s,%x,%x", fname, len, (target_ulong)gdb_flags, (target_ulong)mode); } static void gdb_close(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { gdb_do_syscall(complete, "close,%x", (target_ulong)gf->hostfd); } static void gdb_read(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { gdb_do_syscall(complete, "read,%x,%x,%x", (target_ulong)gf->hostfd, buf, len); } static void gdb_write(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { gdb_do_syscall(complete, "write,%x,%x,%x", (target_ulong)gf->hostfd, buf, len); } static void gdb_lseek(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, int64_t off, int gdb_whence) { gdb_do_syscall(complete, "lseek,%x,%lx,%x", (target_ulong)gf->hostfd, off, (target_ulong)gdb_whence); } static void gdb_isatty(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { gdb_do_syscall(complete, "isatty,%x", (target_ulong)gf->hostfd); } static void gdb_fstat(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong addr) { gdb_do_syscall(complete, "fstat,%x,%x", (target_ulong)gf->hostfd, addr); } static void gdb_stat(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, target_ulong addr) { int len = validate_strlen(cs, fname, fname_len); if (len < 0) { complete(cs, -1, -len); return; } gdb_do_syscall(complete, "stat,%s,%x", fname, len, addr); } static void gdb_remove(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len) { int len = validate_strlen(cs, fname, fname_len); if (len < 0) { complete(cs, -1, -len); return; } gdb_do_syscall(complete, "unlink,%s", fname, len); } static void gdb_rename(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong oname, target_ulong oname_len, target_ulong nname, target_ulong nname_len) { int olen, nlen; olen = validate_strlen(cs, oname, oname_len); if (olen < 0) { complete(cs, -1, -olen); return; } nlen = validate_strlen(cs, nname, nname_len); if (nlen < 0) { complete(cs, -1, -nlen); return; } gdb_do_syscall(complete, "rename,%s,%s", oname, olen, nname, nlen); } static void gdb_system(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong cmd, target_ulong cmd_len) { int len = validate_strlen(cs, cmd, cmd_len); if (len < 0) { complete(cs, -1, -len); return; } gdb_do_syscall(complete, "system,%s", cmd, len); } static void gdb_gettimeofday(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong tv_addr, target_ulong tz_addr) { gdb_do_syscall(complete, "gettimeofday,%x,%x", tv_addr, tz_addr); } /* * Host semihosting syscall implementations. */ static void host_open(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, int gdb_flags, int mode) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char *p; int ret, host_flags; ret = validate_lock_user_string(&p, cs, fname, fname_len); if (ret < 0) { complete(cs, -1, -ret); return; } if (gdb_flags & GDB_O_WRONLY) { host_flags = O_WRONLY; } else if (gdb_flags & GDB_O_RDWR) { host_flags = O_RDWR; } else { host_flags = O_RDONLY; } if (gdb_flags & GDB_O_CREAT) { host_flags |= O_CREAT; } if (gdb_flags & GDB_O_TRUNC) { host_flags |= O_TRUNC; } if (gdb_flags & GDB_O_EXCL) { host_flags |= O_EXCL; } ret = open(p, host_flags, mode); if (ret < 0) { complete(cs, -1, errno); } else { int guestfd = alloc_guestfd(); associate_guestfd(guestfd, ret); complete(cs, guestfd, 0); } unlock_user(p, fname, 0); } static void host_close(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { /* * Only close the underlying host fd if it's one we opened on behalf * of the guest in SYS_OPEN. */ if (gf->hostfd != STDIN_FILENO && gf->hostfd != STDOUT_FILENO && gf->hostfd != STDERR_FILENO && close(gf->hostfd) < 0) { complete(cs, -1, errno); } else { complete(cs, 0, 0); } } static void host_read(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; void *ptr = lock_user(VERIFY_WRITE, buf, len, 0); ssize_t ret; if (!ptr) { complete(cs, -1, EFAULT); return; } do { ret = read(gf->hostfd, ptr, len); } while (ret == -1 && errno == EINTR); if (ret == -1) { complete(cs, -1, errno); unlock_user(ptr, buf, 0); } else { complete(cs, ret, 0); unlock_user(ptr, buf, ret); } } static void host_write(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; void *ptr = lock_user(VERIFY_READ, buf, len, 1); ssize_t ret; if (!ptr) { complete(cs, -1, EFAULT); return; } ret = write(gf->hostfd, ptr, len); complete(cs, ret, ret == -1 ? errno : 0); unlock_user(ptr, buf, 0); } static void host_lseek(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, int64_t off, int whence) { /* So far, all hosts use the same values. */ QEMU_BUILD_BUG_ON(GDB_SEEK_SET != SEEK_SET); QEMU_BUILD_BUG_ON(GDB_SEEK_CUR != SEEK_CUR); QEMU_BUILD_BUG_ON(GDB_SEEK_END != SEEK_END); off_t ret = off; int err = 0; if (ret == off) { ret = lseek(gf->hostfd, ret, whence); if (ret == -1) { err = errno; } } else { ret = -1; err = EINVAL; } complete(cs, ret, err); } static void host_isatty(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { int ret = isatty(gf->hostfd); complete(cs, ret, ret ? 0 : errno); } static void host_flen(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { struct stat buf; if (fstat(gf->hostfd, &buf) < 0) { complete(cs, -1, errno); } else { complete(cs, buf.st_size, 0); } } static void host_fstat(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong addr) { struct stat buf; int ret; ret = fstat(gf->hostfd, &buf); if (ret) { complete(cs, -1, errno); return; } ret = copy_stat_to_user(cs, addr, &buf); complete(cs, ret ? -1 : 0, ret ? -ret : 0); } static void host_stat(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, target_ulong addr) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; struct stat buf; char *name; int ret, err; ret = validate_lock_user_string(&name, cs, fname, fname_len); if (ret < 0) { complete(cs, -1, -ret); return; } ret = stat(name, &buf); if (ret) { err = errno; } else { ret = copy_stat_to_user(cs, addr, &buf); err = 0; if (ret < 0) { err = -ret; ret = -1; } } complete(cs, ret, err); unlock_user(name, fname, 0); } static void host_remove(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char *p; int ret; ret = validate_lock_user_string(&p, cs, fname, fname_len); if (ret < 0) { complete(cs, -1, -ret); return; } ret = remove(p); complete(cs, ret, ret ? errno : 0); unlock_user(p, fname, 0); } static void host_rename(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong oname, target_ulong oname_len, target_ulong nname, target_ulong nname_len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char *ostr, *nstr; int ret; ret = validate_lock_user_string(&ostr, cs, oname, oname_len); if (ret < 0) { complete(cs, -1, -ret); return; } ret = validate_lock_user_string(&nstr, cs, nname, nname_len); if (ret < 0) { unlock_user(ostr, oname, 0); complete(cs, -1, -ret); return; } ret = rename(ostr, nstr); complete(cs, ret, ret ? errno : 0); unlock_user(ostr, oname, 0); unlock_user(nstr, nname, 0); } static void host_system(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong cmd, target_ulong cmd_len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; char *p; int ret; ret = validate_lock_user_string(&p, cs, cmd, cmd_len); if (ret < 0) { complete(cs, -1, -ret); return; } ret = system(p); complete(cs, ret, ret == -1 ? errno : 0); unlock_user(p, cmd, 0); } static void host_gettimeofday(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong tv_addr, target_ulong tz_addr) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; struct gdb_timeval *p; int64_t rt; /* GDB fails on non-null TZ, so be consistent. */ if (tz_addr != 0) { complete(cs, -1, EINVAL); return; } p = lock_user(VERIFY_WRITE, tv_addr, sizeof(struct gdb_timeval), 0); if (!p) { complete(cs, -1, EFAULT); return; } /* TODO: Like stat, gdb always produces big-endian results; match it. */ rt = g_get_real_time(); p->tv_sec = cpu_to_be32(rt / G_USEC_PER_SEC); p->tv_usec = cpu_to_be64(rt % G_USEC_PER_SEC); unlock_user(p, tv_addr, sizeof(struct gdb_timeval)); } /* * Static file semihosting syscall implementations. */ static void staticfile_read(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { CPUArchState *env G_GNUC_UNUSED = cs->env_ptr; target_ulong rest = gf->staticfile.len - gf->staticfile.off; void *ptr; if (len > rest) { len = rest; } ptr = lock_user(VERIFY_WRITE, buf, len, 0); if (!ptr) { complete(cs, -1, EFAULT); return; } memcpy(ptr, gf->staticfile.data + gf->staticfile.off, len); gf->staticfile.off += len; complete(cs, len, 0); unlock_user(ptr, buf, len); } static void staticfile_lseek(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, int64_t off, int gdb_whence) { int64_t ret; switch (gdb_whence) { case GDB_SEEK_SET: ret = off; break; case GDB_SEEK_CUR: ret = gf->staticfile.off + off; break; case GDB_SEEK_END: ret = gf->staticfile.len + off; break; default: ret = -1; break; } if (ret >= 0 && ret <= gf->staticfile.len) { gf->staticfile.off = ret; complete(cs, ret, 0); } else { complete(cs, -1, EINVAL); } } static void staticfile_flen(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf) { complete(cs, gf->staticfile.len, 0); } /* * Syscall entry points. */ void semihost_sys_open(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, int gdb_flags, int mode) { if (use_gdb_syscalls()) { gdb_open(cs, complete, fname, fname_len, gdb_flags, mode); } else { host_open(cs, complete, fname, fname_len, gdb_flags, mode); } } void semihost_sys_close(CPUState *cs, gdb_syscall_complete_cb complete, int fd) { GuestFD *gf = get_guestfd(fd); if (!gf) { complete(cs, -1, EBADF); return; } switch (gf->type) { case GuestFDGDB: gdb_close(cs, complete, gf); break; case GuestFDHost: host_close(cs, complete, gf); break; case GuestFDStatic: complete(cs, 0, 0); break; default: g_assert_not_reached(); } dealloc_guestfd(fd); } void semihost_sys_read_gf(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { /* * Bound length for 64-bit guests on 32-bit hosts, not overlowing ssize_t. * Note the Linux kernel does this with MAX_RW_COUNT, so it's not a bad * idea to do this unconditionally. */ if (len > INT32_MAX) { len = INT32_MAX; } switch (gf->type) { case GuestFDGDB: gdb_read(cs, complete, gf, buf, len); break; case GuestFDHost: host_read(cs, complete, gf, buf, len); break; case GuestFDStatic: staticfile_read(cs, complete, gf, buf, len); break; default: g_assert_not_reached(); } } void semihost_sys_read(CPUState *cs, gdb_syscall_complete_cb complete, int fd, target_ulong buf, target_ulong len) { GuestFD *gf = get_guestfd(fd); if (gf) { semihost_sys_read_gf(cs, complete, gf, buf, len); } else { complete(cs, -1, EBADF); } } void semihost_sys_write_gf(CPUState *cs, gdb_syscall_complete_cb complete, GuestFD *gf, target_ulong buf, target_ulong len) { /* * Bound length for 64-bit guests on 32-bit hosts, not overlowing ssize_t. * Note the Linux kernel does this with MAX_RW_COUNT, so it's not a bad * idea to do this unconditionally. */ if (len > INT32_MAX) { len = INT32_MAX; } switch (gf->type) { case GuestFDGDB: gdb_write(cs, complete, gf, buf, len); break; case GuestFDHost: host_write(cs, complete, gf, buf, len); break; case GuestFDStatic: /* Static files are never open for writing: EBADF. */ complete(cs, -1, EBADF); break; default: g_assert_not_reached(); } } void semihost_sys_write(CPUState *cs, gdb_syscall_complete_cb complete, int fd, target_ulong buf, target_ulong len) { GuestFD *gf = get_guestfd(fd); if (gf) { semihost_sys_write_gf(cs, complete, gf, buf, len); } else { complete(cs, -1, EBADF); } } void semihost_sys_lseek(CPUState *cs, gdb_syscall_complete_cb complete, int fd, int64_t off, int gdb_whence) { GuestFD *gf = get_guestfd(fd); if (!gf) { complete(cs, -1, EBADF); return; } switch (gf->type) { case GuestFDGDB: gdb_lseek(cs, complete, gf, off, gdb_whence); return; case GuestFDHost: host_lseek(cs, complete, gf, off, gdb_whence); break; case GuestFDStatic: staticfile_lseek(cs, complete, gf, off, gdb_whence); break; default: g_assert_not_reached(); } } void semihost_sys_isatty(CPUState *cs, gdb_syscall_complete_cb complete, int fd) { GuestFD *gf = get_guestfd(fd); if (!gf) { complete(cs, 0, EBADF); return; } switch (gf->type) { case GuestFDGDB: gdb_isatty(cs, complete, gf); break; case GuestFDHost: host_isatty(cs, complete, gf); break; case GuestFDStatic: complete(cs, 0, ENOTTY); break; default: g_assert_not_reached(); } } void semihost_sys_flen(CPUState *cs, gdb_syscall_complete_cb fstat_cb, gdb_syscall_complete_cb flen_cb, int fd, target_ulong fstat_addr) { GuestFD *gf = get_guestfd(fd); if (!gf) { flen_cb(cs, -1, EBADF); return; } switch (gf->type) { case GuestFDGDB: gdb_fstat(cs, fstat_cb, gf, fstat_addr); break; case GuestFDHost: host_flen(cs, flen_cb, gf); break; case GuestFDStatic: staticfile_flen(cs, flen_cb, gf); break; default: g_assert_not_reached(); } } void semihost_sys_fstat(CPUState *cs, gdb_syscall_complete_cb complete, int fd, target_ulong addr) { GuestFD *gf = get_guestfd(fd); if (!gf) { complete(cs, -1, EBADF); return; } switch (gf->type) { case GuestFDGDB: gdb_fstat(cs, complete, gf, addr); break; case GuestFDHost: host_fstat(cs, complete, gf, addr); break; case GuestFDStatic: default: g_assert_not_reached(); } } void semihost_sys_stat(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len, target_ulong addr) { if (use_gdb_syscalls()) { gdb_stat(cs, complete, fname, fname_len, addr); } else { host_stat(cs, complete, fname, fname_len, addr); } } void semihost_sys_remove(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong fname, target_ulong fname_len) { if (use_gdb_syscalls()) { gdb_remove(cs, complete, fname, fname_len); } else { host_remove(cs, complete, fname, fname_len); } } void semihost_sys_rename(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong oname, target_ulong oname_len, target_ulong nname, target_ulong nname_len) { if (use_gdb_syscalls()) { gdb_rename(cs, complete, oname, oname_len, nname, nname_len); } else { host_rename(cs, complete, oname, oname_len, nname, nname_len); } } void semihost_sys_system(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong cmd, target_ulong cmd_len) { if (use_gdb_syscalls()) { gdb_system(cs, complete, cmd, cmd_len); } else { host_system(cs, complete, cmd, cmd_len); } } void semihost_sys_gettimeofday(CPUState *cs, gdb_syscall_complete_cb complete, target_ulong tv_addr, target_ulong tz_addr) { if (use_gdb_syscalls()) { gdb_gettimeofday(cs, complete, tv_addr, tz_addr); } else { host_gettimeofday(cs, complete, tv_addr, tz_addr); } }