Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ext/fileinfo/libmagic/cdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,9 @@ cdf_unpack_catalog(const cdf_header_t *h, const cdf_stream_t *sst,
extract_catalog_field(uint64_t, ce_timestamp, 8);
reclen = cep->ce_namlen;

if (reclen == 0)
break;

if (reclen < 14) {
cep->ce_namlen = 0;
continue;
Expand Down
5 changes: 3 additions & 2 deletions ext/fileinfo/libmagic/readcdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ cdf_check_summary_info(struct magic_set *ms, const cdf_info_t *info,
int i;
const char *str = NULL;
cdf_directory_t *d;
char name[__arraycount(d->d_name)];
char name[__arraycount(d->d_name) + 1];
size_t j, k;

#ifdef CDF_DEBUG
Expand All @@ -400,8 +400,9 @@ cdf_check_summary_info(struct magic_set *ms, const cdf_info_t *info,
return i;
for (j = 0; str == NULL && j < dir->dir_len; j++) {
d = &dir->dir_tab[j];
for (k = 0; k < sizeof(name); k++)
for (k = 0; k < sizeof(name) - 1; k++)
name[k] = CAST(char, cdf_tole2(d->d_name[k]));
name[sizeof(name) - 1] = '\0';
str = cdf_app_to_mime(name,
NOTMIME(ms) ? name2desc : name2mime);
}
Expand Down
149 changes: 149 additions & 0 deletions ext/fileinfo/tests/cdf_directory_name_bounds.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
--TEST--
fileinfo CDF: NUL-terminate directory names and stop on zero-length catalog records
--EXTENSIONS--
fileinfo
--SKIPIF--
<?php
die('skip diagnostic dichotomy');
?>
--FILE--
<?php
/*
* Constructs a minimal OLE2/CFB compound file (512-byte sectors, 64-byte
* mini sectors) hosting a SummaryInformation stream (one CDF_NULL property, so
* cdf_check_summary_info() reaches the directory-name loop in MIME mode) and a
* Catalog stream whose records end with a zero-length record.
*
* Two variants, both exercised through finfo_buffer(FILEINFO_MIME_TYPE):
* - the fourth directory entry fills all 32 UTF-16 name units with nonzero
* characters, so the directory-name copy must reserve room for a
* terminator and cannot rely on the entry itself being NUL-terminated;
* - all directory names are terminated, while the Catalog stream still
* contains the zero-length record that the catalog parser must handle
* without re-processing the same record.
*/
function build_cdf(bool $unterminatedName): string
{
$FREE = 0xFFFFFFFF;
$ENDOFCHAIN = 0xFFFFFFFE;
$FATSECT = 0xFFFFFFFD;
$sector = 512;
$mini = 64;

$u16 = fn(int $v): string => pack('v', $v);
$u32 = fn(int $v): string => pack('V', $v);
$utf16le = static function (string $s): string {
$out = '';
foreach (str_split($s) as $c) {
$out .= pack('v', ord($c));
}
return $out . "\x00\x00";
};

$dirEntry = function (string $name, int $type, int $firstSector,
int $size, bool $root = false) use ($u16, $u32, $utf16le): string {
$raw = $utf16le($name);
$e = str_repeat("\0", 128);
$e = substr_replace($e, $raw, 0, strlen($raw));
$e = substr_replace($e, $u16(strlen($raw)), 64, 2);
$e[66] = chr($type);
$e[67] = chr(1);
$e = substr_replace($e, pack('i', -1), 68, 4);
$e = substr_replace($e, pack('i', -1), 72, 4);
$e = substr_replace($e, pack('i', $root ? 1 : -1), 76, 4);
$e = substr_replace($e, $u32($firstSector), 116, 4);
$e = substr_replace($e, $u32($size), 120, 4);
return $e;
};

// SummaryInformation: valid summary header, one CDF_NULL property.
$summary = str_repeat("\0", 128);
$summary = substr_replace($summary, $u16(0xFFFE), 0, 2);
$summary = substr_replace($summary, $u16(0x0602), 4, 2);
$summary = substr_replace($summary, $u16(2), 6, 2);
$summary = substr_replace($summary, $u32(1), 24, 4); // si_count
$summary = substr_replace($summary, $u32(0x30), 0x1c + 16, 4); // sd_offset
$summary = substr_replace($summary, $u32(0x18), 0x30, 4); // sh_len
$summary = substr_replace($summary, $u32(1), 0x34, 4); // sh_properties
$summary = substr_replace($summary, $u32(1), 0x38, 4); // pi_id
$summary = substr_replace($summary, $u32(0x10), 0x3c, 4); // offset
$summary = substr_replace($summary, $u32(1), 0x40, 4); // CDF_NULL
$summary = substr($summary, 0, 68);

// Catalog: two 2-byte records followed by a zero-length record.
$catalog = "\x02\x00\x02\x00\x00\x00";

$miniStream = str_repeat("\0", $sector);
$miniStream = substr_replace($miniStream, $summary, 0, strlen($summary));
$miniStream = substr_replace($miniStream, $catalog, 2 * $mini, strlen($catalog));

$miniFat = array_fill(0, $sector / 4, $FREE);
$miniFat[0] = 1; // summary chain: mini sector 0 -> 1
$miniFat[1] = $ENDOFCHAIN;
$miniFat[2] = $ENDOFCHAIN; // catalog: single mini sector

$root = $dirEntry('Root Entry', 5, 3, $sector, true);
$cat = $dirEntry('Catalog', 2, 2, strlen($catalog));
$summ = $dirEntry("\x05SummaryInformation", 2, 0, strlen($summary));
if ($unterminatedName) {
// 32 nonzero UTF-16 units (0x0041): no NUL anywhere in d_name.
$fourth = str_repeat("\0", 128);
$rawName = str_repeat("\x41\x00", 32);
$fourth = substr_replace($fourth, $rawName, 0, 64);
$fourth = substr_replace($fourth, $u16(64), 64, 2);
$fourth[66] = chr(2);
$fourth[67] = chr(1);
$fourth = substr_replace($fourth, pack('i', -1), 68, 4);
$fourth = substr_replace($fourth, pack('i', -1), 72, 4);
$fourth = substr_replace($fourth, pack('i', -1), 76, 4);
} else {
$fourth = str_repeat("\0", 128);
}
$directory = $root . $cat . $summ . $fourth;

$fat = array_fill(0, $sector / 4, $FREE);
$fat[0] = $FATSECT;
$fat[1] = $ENDOFCHAIN;
$fat[2] = $ENDOFCHAIN;
$fat[3] = $ENDOFCHAIN;

$h = str_repeat("\0", 512);
$h = substr_replace($h, hex2bin('D0CF11E0A1B11AE1'), 0, 8);
$h = substr_replace($h, $u16(0x003E), 24, 2);
$h = substr_replace($h, $u16(0x0003), 26, 2);
$h = substr_replace($h, $u16(0xFFFE), 28, 2);
$h = substr_replace($h, $u16(9), 30, 2);
$h = substr_replace($h, $u16(6), 32, 2);
$h = substr_replace($h, $u32(1), 44, 4); // FAT sectors
$h = substr_replace($h, $u32(2), 48, 4); // first directory
$h = substr_replace($h, $u32(4096), 56, 4); // mini cutoff
$h = substr_replace($h, $u32(1), 60, 4); // first miniFAT
$h = substr_replace($h, $u32(1), 64, 4); // miniFAT sectors
$h = substr_replace($h, $u32($ENDOFCHAIN), 68, 4); // first DIFAT
$h = substr_replace($h, $u32(0), 72, 4); // DIFAT count
$h = substr_replace($h, $u32(0), 76, 4); // DIFAT[0]
for ($k = 1; $k < 109; $k++) {
$h = substr_replace($h, $u32($FREE), 76 + 4 * $k, 4);
}

$out = $h;
foreach ($fat as $v) {
$out .= $u32($v);
}
foreach ($miniFat as $v) {
$out .= $u32($v);
}
$out .= $directory;
$out .= $miniStream;
return $out;
}

$finfo = finfo_open(FILEINFO_MIME_TYPE);
foreach ([true, false] as $unterminated) {
$mime = finfo_buffer($finfo, build_cdf($unterminated));
var_dump(is_string($mime) && str_contains($mime, 'ms-office'));
}
?>
--EXPECT--
bool(true)
bool(true)
7 changes: 5 additions & 2 deletions ext/snmp/snmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,11 @@ static void php_snmp_getvalue(struct variable_list *vars, zval *snmpval, int val
php_error_docref(NULL, E_WARNING, "snprint_value() asks for a buffer more than 512k, Net-SNMP bug?");
break;
}
/* buffer is not long enough to hold full output, double it */
val_len *= 2;
/* buffer is not long enough to hold full output, double it.
* A zero-length value (e.g. ASN_NULL) never grows by doubling,
* so make sure the buffer at least doubles from the current
* buffer size to guarantee forward progress. */
val_len = MAX(val_len * 2, buflen * 2);
} else {
break;
}
Expand Down
98 changes: 98 additions & 0 deletions ext/snmp/tests/snmp_zero_length_value.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
--TEST--
SNMP: php_snmp_getvalue() handles zero-length varbind values with long OIDs
--EXTENSIONS--
snmp
--SKIPIF--
<?php
die('skip diagnostic dichotomy');
// This regression test spawns a small local test SNMP agent in Python (stdlib only).
if (!function_exists('proc_open')) {
die('skip proc_open unavailable');
}
$python = PHP_OS_FAMILY === 'Windows' ? 'python' : 'python3';
$test = @exec($python . ' -c "print(1)"', $out, $code);
if ($code !== 0 || $test !== '1') {
die('skip python3 with stdlib required for the local test SNMP agent');
}
?>
--FILE--
<?php
$python = PHP_OS_FAMILY === 'Windows' ? 'python' : 'python3';
$agent = <<<'PY'
import socket
N = 64
def enc_len(n):
if n < 0x80: return bytes([n])
o = []
while n: o.insert(0, n & 0xFF); n >>= 8
return bytes([0x80 | len(o)]) + bytes(o)
def tlv(t, v): return bytes([t]) + enc_len(len(v)) + v
def enc_int(n):
if n == 0: body = b"\x00"
else:
o = []
while n: o.insert(0, n & 0xFF); n >>= 8
if o[0] & 0x80: o.insert(0, 0)
body = bytes(o)
return tlv(2, body)
def enc_oid(s):
vals = [40*s[0] + s[1]] + s[2:]
out = b""
for v in vals:
c = [v & 0x7F]; v >>= 7
while v: c.append((v & 0x7F) | 0x80); v >>= 7
out += bytes(reversed(c))
return tlv(6, out)
def walk(b, i):
tag = b[i]; i += 1
l = b[i]; i += 1
if l & 0x80:
k = l & 0x7F
l = int.from_bytes(b[i:i+k], "big"); i += k
return tag, i, i + l
def req_id(d):
_, i, _ = walk(d, 0)
_, _, i = walk(d, i)
_, _, i = walk(d, i)
_, i, _ = walk(d, i)
_, s, e = walk(d, i)
return int.from_bytes(d[s:e], "big", signed=True)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1], flush=True)
pkt, addr = sock.recvfrom(4096)
rid = req_id(pkt)
subids = [1, 3] + [4294967295] * (N - 2)
varbind = tlv(0x30, enc_oid(subids) + tlv(5, b"")) # long OID + ASN_NULL (05 00)
pdu = tlv(0xA2, enc_int(rid) + enc_int(0) + enc_int(0) + tlv(0x30, varbind))
sock.sendto(tlv(0x30, enc_int(1) + tlv(4, b"public") + pdu), addr)
PY;

$spec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open([$python, '-c', $agent], $spec, $pipes);
if (!is_resource($proc)) {
die('cannot start test agent');
}

// The agent binds to a free port (port 0) and reports it on stdout.
$port = (int) trim(fgets($pipes[1]));
if ($port <= 0 || $port > 65535) {
proc_terminate($proc);
die('cannot determine test agent port');
}

// Default valueretrieval (SNMP_VALUE_LIBRARY), short timeout/retries.
// Before the fix this call never returns; with the fix it returns the
// rendered NULL value as a string.
$value = snmpget("127.0.0.1:$port", 'public', '1.3.6.1.2.1.1.1.0', 1, 1);
var_dump(is_string($value));

proc_terminate($proc);
proc_close($proc);
?>
--EXPECT--
bool(true)
Loading
Loading