On Sat, Apr 17, 2021 at 07:36:39AM -0700, Alexei Starovoitov wrote: > The kernel will perform the same work with FDs. The same locks are held > and the same execution conditions are in both cases. The LSM hooks, > fsnotify, etc will be called the same way. > It's no different if new syscall was introduced "sys_foo(int num)" that > would do { return close_fd(num); }. > It would opearate in the same user context. Hmm... unless I'm misreading the code, one of the call chains would seem to be sys_bpf() -> bpf_prog_test_run() -> ->test_run() -> ... -> bpf_sys_close(). OK, as long as you make sure bpf_prog_get() does fdput() (i.e. that we don't have it restructured so that fdget()/fdput() pair would be lifted into bpf_prog_test_run(), with fdput() moved in place of bpf_prog_put()). Note that we *really* can not allow close_fd() on anything to be bracketed by fdget()/fdput() pair; we had bugs of that sort and, as the matter of fact, still have one in autofs_dev_ioctl(). The trouble happens if you have file F with 2 references, held by descriptor tables of different processes. Say, process A has descriptor 6 refering to it, while B has descriptor 42 doing the same. Descriptor tables of A and B are not shared with anyone. A: fdget(6) -> returns a reference to F, refcount _not_ touched A: close_fd(6) -> rips the reference to F from descriptor table, does fput(F) refcount drops to 1. B: close(42) -> rips the reference to F from B's descriptor table, does fput(F) This time refcount does reach 0 and we use task_work_add() to make sure the destructor (__fput()) runs before B returns to userland. sys_close() returns and B goes off to userland. On the way out __fput() is run, and among other things, ->release() of F is executed, doing whatever it wants to do. F is freed. And at that point A, which presumably is using the guts of F, gets screwed. In case of autofs_dev_ioctl(), it's possible for a thread to end up blocked inside copy_to_user(), with autofs functions in call chains *AND* module refcount of autofs not pinned by anything. The effects of returning into a function that used to reside in now unmapped page are obviously not pretty... Basically, the rule is * never remove from descriptor table if you currently have an outstadning fdget() (i.e. without having done the matching fdput() yet). That, obviously, covers all ioctls - there we have fdget() done by sys_ioctl() on the issuing descriptor. In your case you seem to be safe, but it's a bloody dangerous minefield - you really need a big warning in all call sites. The worst part is that it won't happen with intended use, so it doesn't show up in routine regression testing. In particular, for autofs the normal case is AUTOFS_DEV_IOCTL_CLOSEMOUNT getting passed a file descriptor of something mounted and *not* the descriptor of /dev/autofs we are holding fdget() on. However, there's no way to prevent a malicious call when we pass exactly that. So please, mark all call sites with "make very sure you never get here with unpaired fdget()". BTW, if my reading (re ->test_run()) is correct, what limits the recursion via bpf_sys_bpf()?