Instead of reading only the amount that will fit in the caller's buffer, just fill your entire own local buffer.  At worst you need 1 byte of lookahead.  So at worst when you find a '\r' and the local buffer is empty you only need to slide the '\r' to the beginning of the local buffer, fill the rest, and continue your scan.

I think that makes your main loop for read look something like this:

  private final byte[] buf = new byte[8192];
  private final InputStream in;
  private int cnt;
  private int ptr;

  public int read(byte[] bs, int off, int len) throws IOException {
    if (len == 0)
      return 0;

    final int startOff = off;
    final int end = off + len;

    while (off < end) {
      if (ptr == cnt) {
        cnt = in.read(buf, 0, buf.length);
        if (cnt < 1)
          break;
        ptr = 0;
      }

      byte b = buf[ptr++];
      if (b != '\r') {
        bs[off++] = b;
        continue;
      }

      if (ptr == cnt) {
        cnt = in.read(buf, 1, buf.length - 1);
        if (cnt <= 0) {
          bs[off++] = '\r';
          break;
        }
        buf[0] = '\r';
        ptr = 1;
      }

      if (buf[ptr] == '\n') {
        bs[off++] = '\n';
        ptr++;
      } else
        bs[off++] = '\r';
    }

    return startOff == off ? -1 : off - startOff;
  }