From: Dmitry V. Levin <hidden> Date: 2025-01-07 23:04:57
PTRACE_SET_SYSCALL_INFO is a generic ptrace API that complements
PTRACE_GET_SYSCALL_INFO by letting the ptracer modify details of
system calls the tracee is blocked in.
This API allows ptracers to obtain and modify system call details
in a straightforward and architecture-agnostic way.
Current implementation supports changing only those bits of system call
information that are used by strace, namely, syscall number, syscall
arguments, and syscall return value.
Support of changing additional details returned by PTRACE_GET_SYSCALL_INFO,
such as instruction pointer and stack pointer, could be added later
if needed, by re-using struct ptrace_syscall_info.reserved to specify
the additional details that should be set. Currently, the reserved
field of struct ptrace_syscall_info must be initialized with zeroes;
arch, instruction_pointer, and stack_pointer fields are ignored.
PTRACE_SET_SYSCALL_INFO currently supports only PTRACE_SYSCALL_INFO_ENTRY,
PTRACE_SYSCALL_INFO_EXIT, and PTRACE_SYSCALL_INFO_SECCOMP operations.
Other operations could be added later if needed.
Ideally, PTRACE_SET_SYSCALL_INFO should have been introduced along with
PTRACE_GET_SYSCALL_INFO, but it didn't happen. The last straw that
convinced me to implement PTRACE_SET_SYSCALL_INFO was apparent failure
to provide an API of changing the first system call argument on riscv
architecture.
ptrace(2) man page:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
...
PTRACE_SET_SYSCALL_INFO
Modify information about the system call that caused the stop.
The "data" argument is a pointer to struct ptrace_syscall_info
that specifies the system call information to be set.
The "addr" argument should be set to sizeof(struct ptrace_syscall_info)).
Link: https://lore.kernel.org/all/59505464-c84a-403d-972f-d4b2055eeaac@gmail.com/
Signed-off-by: Dmitry V. Levin <redacted>
---
include/linux/ptrace.h | 3 ++
include/uapi/linux/ptrace.h | 3 +-
kernel/ptrace.c | 102 ++++++++++++++++++++++++++++++++++++
3 files changed, 107 insertions(+), 1 deletion(-)
@@ -1018,6 +1018,104 @@ ptrace_get_syscall_info(struct task_struct *child, unsigned long user_size,write_size=min(actual_size,user_size);returncopy_to_user(datavp,&info,write_size)?-EFAULT:actual_size;}++staticunsignedlong+ptrace_set_syscall_info_entry(structtask_struct*child,structpt_regs*regs,+structptrace_syscall_info*info)+{+unsignedlongargs[ARRAY_SIZE(info->entry.args)];+intnr=info->entry.nr;+inti;++if(nr!=info->entry.nr)+return-ERANGE;++for(i=0;i<ARRAY_SIZE(args);i++){+args[i]=info->entry.args[i];+if(args[i]!=info->entry.args[i])+return-ERANGE;+}++syscall_set_nr(child,regs,nr);+syscall_set_arguments(child,regs,args);+if(nr==-1){+/*+*Whenthesyscallnumberissetto-1,thesyscallwillbe+*skipped.Inthiscasealsosetthesyscallreturnvalueto+*-ENOSYS,otherwiseonsomearchitecturesthecorresponding+*structpt_regsfieldwillremainunchanged.+*+*Notethatonsomearchitecturessyscall_set_return_value()+*modifiesoneofthestructpt_regsfieldsalsomodifiedby+*syscall_set_arguments(),sotheformershouldbecalled+*afterthelatter.+*/+syscall_set_return_value(child,regs,-ENOSYS,0);+}++return0;+}++staticunsignedlong+ptrace_set_syscall_info_seccomp(structtask_struct*child,structpt_regs*regs,+structptrace_syscall_info*info)+{+/*+*info->entryiscurrentlyasubsetofinfo->seccomp,+*info->seccomp.ret_dataiscurrentlyignored.+*/+returnptrace_set_syscall_info_entry(child,regs,info);+}++staticunsignedlong+ptrace_set_syscall_info_exit(structtask_struct*child,structpt_regs*regs,+structptrace_syscall_info*info)+{+if(info->exit.is_error)+syscall_set_return_value(child,regs,info->exit.rval,0);+else+syscall_set_return_value(child,regs,0,info->exit.rval);++return0;+}++staticint+ptrace_set_syscall_info(structtask_struct*child,unsignedlonguser_size,+void__user*datavp)+{+structpt_regs*regs=task_pt_regs(child);+structptrace_syscall_infoinfo;+interror;++BUILD_BUG_ON(sizeof(structptrace_syscall_info)<PTRACE_SYSCALL_INFO_SIZE_VER0);++if(user_size<PTRACE_SYSCALL_INFO_SIZE_VER0||user_size>PAGE_SIZE)+return-EINVAL;++error=copy_struct_from_user(&info,sizeof(info),datavp,user_size);+if(error)+returnerror;++/* Reserved for future use. */+if(memchr_inv(info.reserved,0,sizeof(info.reserved)))+return-EINVAL;++/* Changing the type of the system call stop is not supported. */+if(ptrace_get_syscall_info_op(child)!=info.op)+return-EINVAL;++switch(info.op){+casePTRACE_SYSCALL_INFO_ENTRY:+returnptrace_set_syscall_info_entry(child,regs,&info);+casePTRACE_SYSCALL_INFO_EXIT:+returnptrace_set_syscall_info_exit(child,regs,&info);+casePTRACE_SYSCALL_INFO_SECCOMP:+returnptrace_set_syscall_info_seccomp(child,regs,&info);+default:+/* Other types of system call stops are not supported. */+return-EINVAL;+}+}#endif /* CONFIG_HAVE_ARCH_TRACEHOOK */intptrace_request(structtask_struct*child,longrequest,
@@ -1236,6 +1334,10 @@ int ptrace_request(struct task_struct *child, long request,casePTRACE_GET_SYSCALL_INFO:ret=ptrace_get_syscall_info(child,addr,datavp);break;++casePTRACE_SET_SYSCALL_INFO:+ret=ptrace_set_syscall_info(child,addr,datavp);+break;#endifcasePTRACE_SECCOMP_GET_FILTER:
kernel/ptrace.c:1053:3: error: call to undeclared function 'syscall_set_return_value'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
1053 | syscall_set_return_value(child, regs, -ENOSYS, 0);
| ^
kernel/ptrace.c:1053:3: note: did you mean 'syscall_get_return_value'?
arch/hexagon/include/asm/syscall.h:56:20: note: 'syscall_get_return_value' declared here
56 | static inline long syscall_get_return_value(struct task_struct *task,
| ^
kernel/ptrace.c:1075:3: error: call to undeclared function 'syscall_set_return_value'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
1075 | syscall_set_return_value(child, regs, info->exit.rval, 0);
| ^
2 errors generated.
vim +/syscall_set_return_value +1053 kernel/ptrace.c
1021
1022 static unsigned long
1023 ptrace_set_syscall_info_entry(struct task_struct *child, struct pt_regs *regs,
1024 struct ptrace_syscall_info *info)
1025 {
1026 unsigned long args[ARRAY_SIZE(info->entry.args)];
1027 int nr = info->entry.nr;
1028 int i;
1029
1030 if (nr != info->entry.nr)
1031 return -ERANGE;
1032
1033 for (i = 0; i < ARRAY_SIZE(args); i++) {
1034 args[i] = info->entry.args[i];
1035 if (args[i] != info->entry.args[i])
1036 return -ERANGE;
1037 }
1038
1039 syscall_set_nr(child, regs, nr);
1040 syscall_set_arguments(child, regs, args);
1041 if (nr == -1) {
1042 /*
1043 * When the syscall number is set to -1, the syscall will be
1044 * skipped. In this case also set the syscall return value to
1045 * -ENOSYS, otherwise on some architectures the corresponding
1046 * struct pt_regs field will remain unchanged.
1047 *
1048 * Note that on some architectures syscall_set_return_value()
1049 * modifies one of the struct pt_regs fields also modified by
1050 * syscall_set_arguments(), so the former should be called
1051 * after the latter.
1052 */
kernel/ptrace.c:1053:3: error: implicit declaration of function 'syscall_set_return_value' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
syscall_set_return_value(child, regs, -ENOSYS, 0);
^
kernel/ptrace.c:1053:3: note: did you mean 'syscall_get_return_value'?
arch/hexagon/include/asm/syscall.h:56:20: note: 'syscall_get_return_value' declared here
static inline long syscall_get_return_value(struct task_struct *task,
^
kernel/ptrace.c:1075:3: error: implicit declaration of function 'syscall_set_return_value' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
syscall_set_return_value(child, regs, info->exit.rval, 0);
^
2 errors generated.
vim +/syscall_set_return_value +1053 kernel/ptrace.c
1021
1022 static unsigned long
1023 ptrace_set_syscall_info_entry(struct task_struct *child, struct pt_regs *regs,
1024 struct ptrace_syscall_info *info)
1025 {
1026 unsigned long args[ARRAY_SIZE(info->entry.args)];
1027 int nr = info->entry.nr;
1028 int i;
1029
1030 if (nr != info->entry.nr)
1031 return -ERANGE;
1032
1033 for (i = 0; i < ARRAY_SIZE(args); i++) {
1034 args[i] = info->entry.args[i];
1035 if (args[i] != info->entry.args[i])
1036 return -ERANGE;
1037 }
1038
1039 syscall_set_nr(child, regs, nr);
1040 syscall_set_arguments(child, regs, args);
1041 if (nr == -1) {
1042 /*
1043 * When the syscall number is set to -1, the syscall will be
1044 * skipped. In this case also set the syscall return value to
1045 * -ENOSYS, otherwise on some architectures the corresponding
1046 * struct pt_regs field will remain unchanged.
1047 *
1048 * Note that on some architectures syscall_set_return_value()
1049 * modifies one of the struct pt_regs fields also modified by
1050 * syscall_set_arguments(), so the former should be called
1051 * after the latter.
1052 */
On Wed, Jan 08, 2025 at 01:04:56AM +0200, Dmitry V. Levin wrote:
PTRACE_SET_SYSCALL_INFO is a generic ptrace API that complements
PTRACE_GET_SYSCALL_INFO by letting the ptracer modify details of
system calls the tracee is blocked in.
This API allows ptracers to obtain and modify system call details
in a straightforward and architecture-agnostic way.
Current implementation supports changing only those bits of system call
information that are used by strace, namely, syscall number, syscall
arguments, and syscall return value.
Support of changing additional details returned by PTRACE_GET_SYSCALL_INFO,
such as instruction pointer and stack pointer, could be added later
if needed, by re-using struct ptrace_syscall_info.reserved to specify
the additional details that should be set. Currently, the reserved
field of struct ptrace_syscall_info must be initialized with zeroes;
arch, instruction_pointer, and stack_pointer fields are ignored.
PTRACE_SET_SYSCALL_INFO currently supports only PTRACE_SYSCALL_INFO_ENTRY,
PTRACE_SYSCALL_INFO_EXIT, and PTRACE_SYSCALL_INFO_SECCOMP operations.
Other operations could be added later if needed.
Ideally, PTRACE_SET_SYSCALL_INFO should have been introduced along with
PTRACE_GET_SYSCALL_INFO, but it didn't happen. The last straw that
convinced me to implement PTRACE_SET_SYSCALL_INFO was apparent failure
to provide an API of changing the first system call argument on riscv
architecture.
It seems prudent to also add a comment about 0x4212 being taken right
after "#define PTRACE_GET_SYSCALL_USER_DISPATCH_CONFIG 0x4211", or the
usage of this ptrace request number may be overlooked on the next update.
I would like to suggest adding flags for changing scno and args right
away; while it is possibly of limited use and seems like an unnecessary
overcomplication, at least changing arguments only seems natural to me,
to avoid possible interaction with scno-related shenanigans that might
present/appear on some kernels and/or architectures. Also, it makes
the aforementioned possible extensions of the interface (changing
of ip/sp) more natural (as in those cases users might definitely want
to avoid touching syscall number/arguments).
+ syscall_set_nr(child, regs, nr);
+ syscall_set_arguments(child, regs, args);
+ if (nr == -1) {
+ /*
+ * When the syscall number is set to -1, the syscall will be
+ * skipped. In this case also set the syscall return value to
+ * -ENOSYS, otherwise on some architectures the corresponding
+ * struct pt_regs field will remain unchanged.
+ *
+ * Note that on some architectures syscall_set_return_value()
+ * modifies one of the struct pt_regs fields also modified by
+ * syscall_set_arguments(), so the former should be called
+ * after the latter.
+ */
+ syscall_set_return_value(child, regs, -ENOSYS, 0);
+ }
This doesn't look nice to me...
We don't need this syscall_set_return_value(ENOSYS) on x86, right?
So perhaps we should move this "if (nr == -1) code into
syscall_set_nr/syscall_set_arguments on those "some architectures" which
actually need it ?
Oleg.
+ syscall_set_nr(child, regs, nr);
+ syscall_set_arguments(child, regs, args);
+ if (nr == -1) {
+ /*
+ * When the syscall number is set to -1, the syscall will be
+ * skipped. In this case also set the syscall return value to
+ * -ENOSYS, otherwise on some architectures the corresponding
+ * struct pt_regs field will remain unchanged.
+ *
+ * Note that on some architectures syscall_set_return_value()
+ * modifies one of the struct pt_regs fields also modified by
+ * syscall_set_arguments(), so the former should be called
+ * after the latter.
+ */
+ syscall_set_return_value(child, regs, -ENOSYS, 0);
+ }
This doesn't look nice to me...
We don't need this syscall_set_return_value(ENOSYS) on x86, right?
No, we don't need this on x86.
So perhaps we should move this "if (nr == -1) code into
syscall_set_nr/syscall_set_arguments on those "some architectures" which
actually need it ?
Thanks for the suggestion. I think the best option is to skip
syscall_set_arguments() invocation in case of nr == -1. It's not just
pointless, but also it would clobber the syscall return value on those
architectures like arm64 that share the same register both for the first
argument of syscall and its return value.
This is what I'm going to implement for the next iteration of the series:
syscall_set_nr(child, regs, nr);
/*
* If the syscall number is set to -1, setting syscall arguments is not
* just pointless, it would also clobber the syscall return value on
* those architectures that share the same register both for the first
* argument of syscall and its return value.
*/
if (nr != -1)
syscall_set_arguments(child, regs, args);
--
ldv