Commit message: Avoid returning CHM file entries that are "blank" because they have embedded null bytes
CVE: CVE-2018-18585
CWE: CWE-476
Vulnerability description: chmd_read_headers in mspack/chmd.c in libmspack before 0.8alpha accepts a filename that has '\0' as its first or second character (such as the "/\0" name).

Project: libmspack
File: chmd.c

static void chmd_close(struct mschm_decompressor *base,
		       struct mschmd_header *chm)
{
  struct mschm_decompressor_p *self = (struct mschm_decompressor_p *) base;
  struct mschmd_file *fi, *nfi;
  struct mspack_system *sys;
  unsigned int i;

  if (!base) return;
  sys = self->system;

  self->error = MSPACK_ERR_OK;

  /* free files */
  for (fi = chm->files; fi; fi = nfi) {
    nfi = fi->next;
    sys->free(fi);
  }
  for (fi = chm->sysfiles; fi; fi = nfi) {
    nfi = fi->next;
    sys->free(fi);
  }

  /* if this CHM was being decompressed, free decompression state */
  if (self->d && (self->d->chm == chm)) {
    if (self->d->infh) sys->close(self->d->infh);
    if (self->d->state) lzxd_free(self->d->state);
    sys->free(self->d);
    self->d = NULL;
  }

  /* if this CHM had a chunk cache, free it and contents */
  if (chm->chunk_cache) {
      for (i = 0; i < chm->num_chunks; i++) sys->free(chm->chunk_cache[i]);
      sys->free(chm->chunk_cache);
  }

  sys->free(chm);
}

Project: libmspack
File: chmd.c

static int chmd_error(struct mschm_decompressor *base) {
  struct mschm_decompressor_p *self = (struct mschm_decompressor_p *) base;
  return (self) ? self->error : MSPACK_ERR_ARGS;
}

Project: libmspack
File: chmd.c

static int chmd_fast_find(struct mschm_decompressor *base,
			  struct mschmd_header *chm, const char *filename,
			  struct mschmd_file *f_ptr, int f_size)
{
    struct mschm_decompressor_p *self = (struct mschm_decompressor_p *) base;
    struct mspack_system *sys;
    struct mspack_file *fh;
    const unsigned char *chunk, *p, *end;
    int err = MSPACK_ERR_OK, result = -1;
    unsigned int n, sec;

    if (!self || !chm || !f_ptr || (f_size != sizeof(struct mschmd_file))) {
	return MSPACK_ERR_ARGS;
    }
    sys = self->system;

    /* clear the results structure */
    memset(f_ptr, 0, f_size);

    if (!(fh = sys->open(sys, chm->filename, MSPACK_SYS_OPEN_READ))) {
	return MSPACK_ERR_OPEN;
    }

    /* go through PMGI chunk hierarchy to reach PMGL chunk */
    if (chm->index_root < chm->num_chunks) {
	n = chm->index_root;
	for (;;) {
	    if (!(chunk = read_chunk(self, chm, fh, n))) {
		sys->close(fh);
		return self->error;
	    }

	    /* search PMGI/PMGL chunk. exit early if no entry found */
	    if ((result = search_chunk(chm, chunk, filename, &p, &end)) <= 0) {
		break;
	    }

	    /* found result. loop around for next chunk if this is PMGI */
	    if (chunk[3] == 0x4C) break; else READ_ENCINT(n);
	}
    }
    else {
	/* PMGL chunks only, search from first_pmgl to last_pmgl */
	for (n = chm->first_pmgl; n <= chm->last_pmgl;
	     n = EndGetI32(&chunk[pmgl_NextChunk]))
	{
	    if (!(chunk = read_chunk(self, chm, fh, n))) {
		err = self->error;
		break;
	    }

	    /* search PMGL chunk. exit if file found */
	    if ((result = search_chunk(chm, chunk, filename, &p, &end)) > 0) {
		break;
	    }

            /* stop simple infinite loops: can't visit the same chunk twice */
            if ((int)n == EndGetI32(&chunk[pmgl_NextChunk])) {
                break;
            }
	}
    }

    /* if we found a file, read it */
    if (result > 0) {
	READ_ENCINT(sec);
	f_ptr->section  = (sec == 0) ? (struct mschmd_section *) &chm->sec0
	                             : (struct mschmd_section *) &chm->sec1;
	READ_ENCINT(f_ptr->offset);
	READ_ENCINT(f_ptr->length);
    }
    else if (result < 0) {
	err = MSPACK_ERR_DATAFORMAT;
    }

    sys->close(fh);
    return self->error = err;

 chunk_end:
    D(("read beyond end of chunk entries"))
    sys->close(fh);
    return self->error = MSPACK_ERR_DATAFORMAT;
}

Project: libmspack
File: chmd.c

static int chmd_init_decomp(struct mschm_decompressor_p *self,
			    struct mschmd_file *file)
{
  int window_size, window_bits, reset_interval, entry, err;
  struct mspack_system *sys = self->system;
  struct mschmd_sec_mscompressed *sec;
  unsigned char *data;
  off_t length, offset;

  sec = (struct mschmd_sec_mscompressed *) file->section;

  /* ensure we have a mscompressed content section */
  err = find_sys_file(self, sec, &sec->content, content_name);
  if (err) return self->error = err;

  /* ensure we have a ControlData file */
  err = find_sys_file(self, sec, &sec->control, control_name);
  if (err) return self->error = err;

  /* read ControlData */
  if (sec->control->length < lzxcd_SIZEOF) {
    D(("ControlData file is too short"))
    return self->error = MSPACK_ERR_DATAFORMAT;
  }
  if (!(data = read_sys_file(self, sec->control))) {
    D(("can't read mscompressed control data file"))
    return self->error;
  }

  /* check LZXC signature */
  if (EndGetI32(&data[lzxcd_Signature]) != 0x43585A4C) {
    sys->free(data);
    return self->error = MSPACK_ERR_SIGNATURE;
  }

  /* read reset_interval and window_size and validate version number */
  switch (EndGetI32(&data[lzxcd_Version])) {
  case 1:
    reset_interval = EndGetI32(&data[lzxcd_ResetInterval]);
    window_size    = EndGetI32(&data[lzxcd_WindowSize]);
    break;
  case 2:
    reset_interval = EndGetI32(&data[lzxcd_ResetInterval]) * LZX_FRAME_SIZE;
    window_size    = EndGetI32(&data[lzxcd_WindowSize])    * LZX_FRAME_SIZE;
    break;
  default:
    D(("bad controldata version"))
    sys->free(data);
    return self->error = MSPACK_ERR_DATAFORMAT;
  }

  /* free ControlData */
  sys->free(data);

  /* find window_bits from window_size */
  switch (window_size) {
  case 0x008000: window_bits = 15; break;
  case 0x010000: window_bits = 16; break;
  case 0x020000: window_bits = 17; break;
  case 0x040000: window_bits = 18; break;
  case 0x080000: window_bits = 19; break;
  case 0x100000: window_bits = 20; break;
  case 0x200000: window_bits = 21; break;
  default:
    D(("bad controldata window size"))
    return self->error = MSPACK_ERR_DATAFORMAT;
  }

  /* validate reset_interval */
  if (reset_interval == 0 || reset_interval % LZX_FRAME_SIZE) {
    D(("bad controldata reset interval"))
    return self->error = MSPACK_ERR_DATAFORMAT;
  }

  /* which reset table entry would we like? */
  entry = file->offset / reset_interval;
  /* convert from reset interval multiple (usually 64k) to 32k frames */
  entry *= reset_interval / LZX_FRAME_SIZE;

  /* read the reset table entry */
  if (read_reset_table(self, sec, entry, &length, &offset)) {
    /* the uncompressed length given in the reset table is dishonest.
     * the uncompressed data is always padded out from the given
     * uncompressed length up to the next reset interval */
    length += reset_interval - 1;
    length &= -reset_interval;
  }
  else {
    /* if we can't read the reset table entry, just start from
     * the beginning. Use spaninfo to get the uncompressed length */
    entry = 0;
    offset = 0;
    err = read_spaninfo(self, sec, &length);
  }
  if (err) return self->error = err;

  /* get offset of compressed data stream:
   * = offset of uncompressed section from start of file
   * + offset of compressed stream from start of uncompressed section
   * + offset of chosen reset interval from start of compressed stream */
  self->d->inoffset = file->section->chm->sec0.offset + sec->content->offset + offset;

  /* set start offset and overall remaining stream length */
  self->d->offset = entry * LZX_FRAME_SIZE;
  length -= self->d->offset;

  /* initialise LZX stream */
  self->d->state = lzxd_init(&self->d->sys, self->d->infh,
			     (struct mspack_file *) self, window_bits,
			     reset_interval / LZX_FRAME_SIZE,
			     4096, length, 0);
  if (!self->d->state) self->error = MSPACK_ERR_NOMEMORY;
  return self->error;
}

Project: libmspack
File: chmd.c

static struct mschmd_header *chmd_open(struct mschm_decompressor *base,
				       const char *filename)
{
  return chmd_real_open(base, filename, 1);
}

Project: libmspack
File: chmd.c

static int find_sys_file(struct mschm_decompressor_p *self,
			 struct mschmd_sec_mscompressed *sec,
			 struct mschmd_file **f_ptr, const char *name)
{
    struct mspack_system *sys = self->system;
    struct mschmd_file result;

    /* already loaded */
    if (*f_ptr) return MSPACK_ERR_OK;

    /* try using fast_find to find the file - return DATAFORMAT error if
     * it fails, or successfully doesn't find the file */
    if (chmd_fast_find((struct mschm_decompressor *) self, sec->base.chm,
		       name, &result, (int)sizeof(result)) || !result.section)
    {
	return MSPACK_ERR_DATAFORMAT;
    }

    if (!(*f_ptr = (struct mschmd_file *) sys->alloc(sys, sizeof(result)))) {
	return MSPACK_ERR_NOMEMORY;
    }

    /* copy result */
    *(*f_ptr) = result;
    (*f_ptr)->filename = (char *) name;

    /* link file into sysfiles list */
    (*f_ptr)->next = sec->base.chm->sysfiles;
    sec->base.chm->sysfiles = *f_ptr;
    return MSPACK_ERR_OK;
}

Project: libmspack
File: chmd.c

  mspack_create_chm_decompressor(struct mspack_system *sys)
{
  struct mschm_decompressor_p *self = NULL;

  if (!sys) sys = mspack_default_system;
  if (!mspack_valid_system(sys)) return NULL;

  if ((self = (struct mschm_decompressor_p *) sys->alloc(sys, sizeof(struct mschm_decompressor_p)))) {
    self->base.open       = &chmd_open;
    self->base.close      = &chmd_close;
    self->base.extract    = &chmd_extract;
    self->base.last_error = &chmd_error;
    self->base.fast_open  = &chmd_fast_open;
    self->base.fast_find  = &chmd_fast_find;
    self->system          = sys;
    self->error           = MSPACK_ERR_OK;
    self->d               = NULL;
  }
  return (struct mschm_decompressor *) self;
}

Project: libmspack
File: chmd.c

void mspack_destroy_chm_decompressor(struct mschm_decompressor *base) {
  struct mschm_decompressor_p *self = (struct mschm_decompressor_p *) base;
  if (self) {
    struct mspack_system *sys = self->system;
    if (self->d) {
      if (self->d->infh)  sys->close(self->d->infh);
      if (self->d->state) lzxd_free(self->d->state);
      sys->free(self->d);
    }
    sys->free(self);
  }
}

Project: libmspack
File: chmd.c

static unsigned char *read_chunk(struct mschm_decompressor_p *self,
				 struct mschmd_header *chm,
				 struct mspack_file *fh,
				 unsigned int chunk_num)
{
    struct mspack_system *sys = self->system;
    unsigned char *buf;

    /* check arguments - most are already checked by chmd_fast_find */
    if (chunk_num >= chm->num_chunks) return NULL;
    
    /* ensure chunk cache is available */
    if (!chm->chunk_cache) {
	size_t size = sizeof(unsigned char *) * chm->num_chunks;
	if (!(chm->chunk_cache = (unsigned char **) sys->alloc(sys, size))) {
	    self->error = MSPACK_ERR_NOMEMORY;
	    return NULL;
	}
	memset(chm->chunk_cache, 0, size);
    }

    /* try to answer out of chunk cache */
    if (chm->chunk_cache[chunk_num]) return chm->chunk_cache[chunk_num];

    /* need to read chunk - allocate memory for it */
    if (!(buf = (unsigned char *) sys->alloc(sys, chm->chunk_size))) {
	self->error = MSPACK_ERR_NOMEMORY;
	return NULL;
    }

    /* seek to block and read it */
    if (sys->seek(fh, (off_t) (chm->dir_offset + (chunk_num * chm->chunk_size)),
		      MSPACK_SYS_SEEK_START))
    {
	self->error = MSPACK_ERR_SEEK;
	sys->free(buf);
	return NULL;
    }
    if (sys->read(fh, buf, (int)chm->chunk_size) != (int)chm->chunk_size) {
	self->error = MSPACK_ERR_READ;
	sys->free(buf);
	return NULL;
    }

    /* check the signature. Is is PMGL or PMGI? */
    if (!((buf[0] == 0x50) && (buf[1] == 0x4D) && (buf[2] == 0x47) &&
	  ((buf[3] == 0x4C) || (buf[3] == 0x49))))
    {
	self->error = MSPACK_ERR_SEEK;
	sys->free(buf);
	return NULL;
    }

    /* all OK. Store chunk in cache and return it */
    return chm->chunk_cache[chunk_num] = buf;
}

Project: libmspack
File: chmd.c

static int read_off64(off_t *var, unsigned char *mem,
		      struct mspack_system *sys, struct mspack_file *fh)
{
#if LARGEFILE_SUPPORT
    *var = EndGetI64(mem);
#else
    *var = EndGetI32(mem);
    if ((*var & 0x80000000) || EndGetI32(mem+4)) {
	sys->message(fh, (char *)largefile_msg);
	return 1;
    }
#endif
    return 0;
}

Project: libmspack
File: chmd.c

static int read_reset_table(struct mschm_decompressor_p *self,
			    struct mschmd_sec_mscompressed *sec,
			    int entry, off_t *length_ptr, off_t *offset_ptr)
{
    struct mspack_system *sys = self->system;
    unsigned char *data;
    unsigned int pos, entrysize;

    /* do we have a ResetTable file? */
    int err = find_sys_file(self, sec, &sec->rtable, rtable_name);
    if (err) return 0;

    /* read ResetTable file */
    if (sec->rtable->length < lzxrt_headerSIZEOF) {
	D(("ResetTable file is too short"))
	return 0;
    }
    if (!(data = read_sys_file(self, sec->rtable))) {
	D(("can't read reset table"))
	return 0;
    }

    /* check sanity of reset table */
    if (EndGetI32(&data[lzxrt_FrameLen]) != LZX_FRAME_SIZE) {
	D(("bad reset table frame length"))
	sys->free(data);
	return 0;
    }

    /* get the uncompressed length of the LZX stream */
    if (read_off64(length_ptr, &data[lzxrt_UncompLen], sys, self->d->infh)) {
	sys->free(data);
	return 0;
    }

    entrysize = EndGetI32(&data[lzxrt_EntrySize]);
    pos = EndGetI32(&data[lzxrt_TableOffset]) + (entry * entrysize);

    /* ensure reset table entry for this offset exists */
    if (entry < EndGetI32(&data[lzxrt_NumEntries]) &&
	pos <= (sec->rtable->length - entrysize))
    {
	switch (entrysize) {
	case 4:
	    *offset_ptr = EndGetI32(&data[pos]);
	    err = 0;
	    break;
	case 8:
	    err = read_off64(offset_ptr, &data[pos], sys, self->d->infh);
	    break;
	default:
	    D(("reset table entry size neither 4 nor 8"))
	    err = 1;
	    break;
	}
    }
    else {
	D(("bad reset interval"))
	err = 1;
    }

    /* free the reset table */
    sys->free(data);

    /* return success */
    return (err == 0);
}

Project: libmspack
File: chmd.c

static int read_spaninfo(struct mschm_decompressor_p *self,
			 struct mschmd_sec_mscompressed *sec,
			 off_t *length_ptr)
{
    struct mspack_system *sys = self->system;
    unsigned char *data;
    
    /* find SpanInfo file */
    int err = find_sys_file(self, sec, &sec->spaninfo, spaninfo_name);
    if (err) return MSPACK_ERR_DATAFORMAT;

    /* check it's large enough */
    if (sec->spaninfo->length != 8) {
	D(("SpanInfo file is wrong size"))
	return MSPACK_ERR_DATAFORMAT;
    }

    /* read the SpanInfo file */
    if (!(data = read_sys_file(self, sec->spaninfo))) {
	D(("can't read SpanInfo file"))
	return self->error;
    }

    /* get the uncompressed length of the LZX stream */
    err = read_off64(length_ptr, data, sys, self->d->infh);
    sys->free(data);
    if (err) return MSPACK_ERR_DATAFORMAT;

    if (*length_ptr <= 0) {
        D(("output length is invalid"))
        return MSPACK_ERR_DATAFORMAT;
    }

    return MSPACK_ERR_OK;
}

Project: libmspack
File: chmd.c

static unsigned char *read_sys_file(struct mschm_decompressor_p *self,
				    struct mschmd_file *file)
{
  struct mspack_system *sys = self->system;
  unsigned char *data = NULL;
  int len;

  if (!file || !file->section || (file->section->id != 0)) {
    self->error = MSPACK_ERR_DATAFORMAT;
    return NULL;
  }

  len = (int) file->length;

  if (!(data = (unsigned char *) sys->alloc(sys, (size_t) len))) {
    self->error = MSPACK_ERR_NOMEMORY;
    return NULL;
  }
  if (sys->seek(self->d->infh, file->section->chm->sec0.offset
		+ file->offset, MSPACK_SYS_SEEK_START))
  {
    self->error = MSPACK_ERR_SEEK;
    sys->free(data);
    return NULL;
  }
  if (sys->read(self->d->infh, data, len) != len) {
    self->error = MSPACK_ERR_READ;
    sys->free(data);
    return NULL;
  }
  return data;
}

Project: libmspack
File: chmd.c

static int search_chunk(struct mschmd_header *chm,
			const unsigned char *chunk,
			const char *filename,
			const unsigned char **result,
			const unsigned char **result_end)
{
    const unsigned char *start, *end, *p;
    unsigned int qr_size, num_entries, qr_entries, qr_density, name_len;
    unsigned int L, R, M, fname_len, entries_off, is_pmgl;
    int cmp;

    fname_len = strlen(filename);

    /* PMGL chunk or PMGI chunk? (note: read_chunk() has already
     * checked the rest of the characters in the chunk signature) */
    if (chunk[3] == 0x4C) {
	is_pmgl = 1;
	entries_off = pmgl_Entries;
    }
    else {
	is_pmgl = 0;
	entries_off = pmgi_Entries;
    }

    /*  Step 1: binary search first filename of each QR entry
     *  - target filename == entry
     *    found file
     *  - target filename < all entries
     *    file not found
     *  - target filename > all entries
     *    proceed to step 2 using final entry
     *  - target filename between two searched entries
     *    proceed to step 2
     */
    qr_size     = EndGetI32(&chunk[pmgl_QuickRefSize]);
    start       = &chunk[chm->chunk_size - 2];
    end         = &chunk[chm->chunk_size - qr_size];
    num_entries = EndGetI16(start);
    qr_density  = 1 + (1 << chm->density);
    qr_entries  = (num_entries + qr_density-1) / qr_density;

    if (num_entries == 0) {
	D(("chunk has no entries"))
	return -1;
    }

    if (qr_size > chm->chunk_size) {
	D(("quickref size > chunk size"))
	return -1;
    }

    *result_end = end;

    if (((int)qr_entries * 2) > (start - end)) {
	D(("WARNING; more quickrefs than quickref space"))
	qr_entries = 0; /* but we can live with it */
    }

    if (qr_entries > 0) {
	L = 0;
	R = qr_entries - 1;
	do {
	    /* pick new midpoint */
	    M = (L + R) >> 1;

	    /* compare filename with entry QR points to */
	    p = &chunk[entries_off + (M ? EndGetI16(start - (M << 1)) : 0)];
	    READ_ENCINT(name_len);
	    if (name_len > (unsigned int) (end - p)) goto chunk_end;
	    cmp = compare(filename, (char *)p, fname_len, name_len);

	    if (cmp == 0) break;
	    else if (cmp < 0) { if (M) R = M - 1; else return 0; }
	    else if (cmp > 0) L = M + 1;
	} while (L <= R);
	M = (L + R) >> 1;

	if (cmp == 0) {
	    /* exact match! */
	    p += name_len;
	    *result = p;
	    return 1;
	}

	/* otherwise, read the group of entries for QR entry M */
	p = &chunk[entries_off + (M ? EndGetI16(start - (M << 1)) : 0)];
	num_entries -= (M * qr_density);
	if (num_entries > qr_density) num_entries = qr_density;
    }
    else {
	p = &chunk[entries_off];
    }

    /* Step 2: linear search through the set of entries reached in step 1.
     * - filename == any entry
     *   found entry
     * - filename < all entries (PMGI) or any entry (PMGL)
     *   entry not found, stop now
     * - filename > all entries
     *   entry not found (PMGL) / maybe found (PMGI)
     * - 
     */
    *result = NULL;
    while (num_entries-- > 0) {
	READ_ENCINT(name_len);
	if (name_len > (unsigned int) (end - p)) goto chunk_end;
	cmp = compare(filename, (char *)p, fname_len, name_len);
	p += name_len;

	if (cmp == 0) {
	    /* entry found */
	    *result = p;
	    return 1;
	}

	if (cmp < 0) {
	    /* entry not found (PMGL) / maybe found (PMGI) */
	    break;
	}

	/* read and ignore the rest of this entry */
	if (is_pmgl) {
	    READ_ENCINT(R); /* skip section */
	    READ_ENCINT(R); /* skip offset */
	    READ_ENCINT(R); /* skip length */
	}
	else {
	    *result = p; /* store potential final result */
	    READ_ENCINT(R); /* skip chunk number */
	}
    }

     /* PMGL? not found. PMGI? maybe found */
     return (is_pmgl) ? 0 : (*result ? 1 : 0);

 chunk_end:
    D(("reached end of chunk data while searching"))
    return -1;
}