Skip to content

gzungetc(): out-of-bounds write when output buffer state is invalid #1289

Description

@ydsa

Description

I found a crash-prone out-of-bounds write in gzungetc() (gzread.c) when internal output-buffer state is invalid in the state->x.have == 0 path.

/* if output buffer empty, put byte at end (allows more pushing) */
if (state->x.have == 0) {
    state->x.have = 1;
    state->x.next = state->out + (state->size << 1) - 1;
    state->x.next[0] = (unsigned char)c;
    state->x.pos--;
    state->past = 0;
    return c;
}

If state->size == 0, state->x.next becomes state->out - 1; if state->out == NULL, the computed pointer is invalid. The subsequent state->x.next[0] write can cause SEGV / ASan underflow.

This was originally discovered by fuzzing through public gz* entry points (original artifact lost), and I reproduced the fault deterministically using a minimal internal-state PoC with ASan/UBSan.

Reproduction

Build zlib with ASan/UBSan in the source directory:

CFLAGS="-O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer" ./configure --static
make -j

Build the PoC (uses internal header/state, e.g. gzguts.h):

gcc -O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer -I. poc.c ./libz.a -o poc

Run:

ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:symbolize=1 ./poc

Before fix: ASan reports an out-of-bounds write in gzungetc at the state->x.next[0] write site.
After fix: no crash; function returns -1 in the invalid-state path.

PoC source (poc.c)

#include <stdio.h>
#include <string.h>
#include "gzguts.h"

int main(void) {
    gz_state state;
    memset(&state, 0, sizeof(state));

    state.mode = GZ_READ;
    state.err = Z_OK;
    state.how = COPY;
    state.x.have = 0;
    state.out = NULL;
    state.size = 0;

    int ret = gzungetc('A', (gzFile)&state);

    printf("ret=%d\n", ret);
    return 0;
}

Proposed fix

Add a defensive guard before pointer arithmetic in that branch and fail early:

if (state->x.have == 0) {
    if (state->out == NULL || state->size == 0)
        return -1;
    state->x.have = 1;
    state->x.next = state->out + (state->size << 1) - 1;
    state->x.next[0] = (unsigned char)c;
    state->x.pos--;
    state->past = 0;
    return c;
}

This is a minimal hardening change: it preserves normal behavior and prevents invalid-state crashes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions