Solaris NFS Server XDR handling vulnerability

Release date: 27-06-2009


1. Summary

There is vulnerability in Solaris 10 kernel NFS implementation resulting in an attacker being able to perform a denial of service attack on an NFS server. One crafted UDP packet is sufficient to send to port 2049 to trigger the kernel panic and core dump. Remote code execution is unlikely though. This vulnerability was only tested on Solaris 10 and 11 x86, but it's likely that it also affects other platforms/versions. The source code references come from the Nevada project cvs web page dated 11/03/2007 (http://cvs.opensolaris.org).


2. Details

There is a bug in the Solaris kernel NFS ACL code executed when client calls setfacl() (possibly also getfacl() and some others if there are any), that may be exploited to perform a denial of service attack on the server. When calling setfacl(), the ACL structure is passed as an argument following the file handle. When the packet gets opened in Ethereal/Wireshark, two fields describing the number of ACL entries can be seen:

  • ACL count (nfsacl.aclcnt, the structure member)
  • Total ACL entries (the XDR's protocol array length).

Under normal condition, they are equal. But if one of them gets modified, the kernel will panic while trying to free memory chunk using wrong size paramater passed to kmem_free() (size that doesn't match the corresponding kmem_alloc()). When allocating kernel memory with kmem_alloc(chunk_addr, size1), and freeing it with kmem_free(chunk_addr, size2), assuming size1 != size2, the system will panic with the crash message similar to the below:

NOTICE: Failed to decode arguments for ACL version 3 procedure ACL3_SETACL
client clientname.domain

panic[cpu0]/thread=d57a8e00:
vmem_hash_delete(dac04690, d5430600, 786492): bad free


d456a970 genunix:vmem_hash_delete+d0 (dac04690, d5430600,)
d456a9ac genunix:vmem_xfree+2b (dac04690, d5430600,)
d456a9c0 genunix:vmem_free+1e (dac04690, d5430600,)
d456a9f4 genunix:kmem_free+36 (d5430600, c003c)
d456aa34 genunix:xdr_array+f6 (d49cd484, d456ab20,)
d456aa7c nfs:xdr_secattr+69 (d49cd484, d456ab18)
d456aa98 nfs:xdr_SETACL3args+4f (d49cd484, d456aad0)
d456aab0 rpcmod:svc_clts_kfreeargs+29 (d49cd400, fa19438c,)
d456ad10 nfssrv:common_dispatch+6ce (d456ad8c, d49cd400,)
d456ad34 nfssrv:acl_dispatch+1f (d456ad8c, d49cd400)
d456adc4 rpcmod:svc_getreq+158 (d49cd400, dad9e2c0)
d456ae0c rpcmod:svc_run+146 (d57a9960)
d456ae2c rpcmod:svc_do_run+6e (1)
d456af84 nfs:nfssys+3fb (e, d2940fc8, d08e, )

3. Code analysis

Below is the fragment of xdr_secattr() function code:

nfs_acl_xdr.c#87 ::

bool_t
xdr_secattr(XDR *xdrs, vsecattr_t *objp)
{
        uint_t count;

        if (!xdruint(xdrs, &objp->vsa_mask))
                return (FALSE);
[1]     if (!xdr_int(xdrs, &objp->vsa_aclcnt))
                return (FALSE);
[2]     if (objp->vsa_aclentp != NULL)
[3]             count = (uint_t)objp->vsa_aclcnt;
        else
[4]             count = 0;
[5]     if (!xdr_array(xdrs, (char **)&objp->vsa_aclentp, &count,
            NFS_ACL_MAX_ENTRIES, sizeof (aclent_t), (xdrproc_t)xdr_aclent))
                return (FALSE);
[6]     if (count != 0 && count != (uint_t)objp->vsa_aclcnt)
                return (FALSE);

That function is executed two times when the SETACL request has come in. First, common_dispatch() function calls it via the SVC_GETARGS macro with the request to decode the arguments wrapped by XDR. At [1], the vsa_aclcnt field is filled with the integer value corresponding to ACL count (see section 2). But at [2], the vsa_aclentp structure is still NULL, so the freshly filled vsa_aclcnt will not be used as the ACL count as it would have been at [3], but the count variable becomes 0. Then, the count variable is used at xdr_array() call at [5], but is ignored anyway, because xdr_array() function reads the array using the array length (see Total ACL entries at in section 2). Then it fills up the vsa_aclentp pointer. This value should normally be the same as ACL count, but is represented by the different four octets in the packet.
Assume the first value has been modified to be different than the second, let's say legitimate value of ACL Count was 5, but has been modified to 65541. In this case, the function will return at [6], causing the control to be given back to the common_dispatch() function. As result was FALSE, the common_dispatch() function will send "NOTICE: Failed to decode arguments..." message that can be seen above the crash dump in section 2, and will call SVC_FREEARGS macro to free the memory allocated by the garbled arguments. The control gets again to the xdr_secattr() function. But the structure at [2] has already been filled up, so the count becomes 65541 instead of 5. The xdr_array() is called again, and it panics at the mem_free() below, because the nodesize is now different for tha same chunk: xdr_array.c#115 ::

        /*
         * the array may need freeing
         */
        if (xdrs->x_op == XDR_FREE) {
                mem_free(*addrp, nodesize);

4. Exploit

The malicious UDP packet makes the system call SETACL with the five ACLs. The ACL count value (see section 2) was modified from 5 to 65541. This will cause the system to crash. The exploit is not attached in this advisory.


5. Fix

There doesn't seem to be a fix available for this issue for now. The possible way o fixing would probably be to assign (uint_t)objp->vsa_aclcnt a real array length somewhere between [5] and [6] in xdr_secattr().


UPDATE: the issue has been fixed after reporting to the vendor: http://sunsolve.sun.com/search/document.do?assetkey=1-26-102965-1