From 5e900a7d34141e627e59f79e2b55d49aea151673 Mon Sep 17 00:00:00 2001 From: Yoo Jae Hyun Date: Tue, 18 Aug 2026 17:41:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(hpux):=20net=5Fio=5Fcounters()=EA=B0=80=20?= =?UTF-8?q?=EB=AF=B8=EC=B4=88=EA=B8=B0=ED=99=94=20=ED=9E=99=EC=9D=84=20NIC?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20(SERVER-1987)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit psutil_net_io_counters()는 ID_ifNumber(논리 인터페이스 수)만큼 루프를 돌지만, get_physical_stat()이 실제로 채우는 항목 수는 그보다 적을 수 있다. whaHP_IA(HP-UX 11.23 IA) 실측: ifNumber=4 인데 채워지는 것은 lan0/lan1/lo0 3개. 버퍼를 malloc으로 잡아 초기화하지 않았기 때문에 4번째 항목은 이전 힙 내용이 그대로 남아 있었고, nm_device[0]=='\0' 가드도 통과해 미초기화 메모리가 인터페이스 이름으로 반환됐다. 실측 40회 호출 40회 모두 재현: 'lo0', 'lan1', 'lan0', '~\xe6\xbb\x80~\xe6\xbb\xa0~\xe6\xbb\xc0~\xe6\xbb\xe0~\xe6\xbc' 바이트를 뜯어보면 32바이트 간격의 빅엔디안 32비트 힙 포인터 배열이다. 이 이름은 python-infra 에이전트에서 'ifconfig ' + name 형태로 셸에 전달되어, '>' 가 섞이면 작업 디렉토리에 파일을 만들고 다른 메타문자면 root 권한 명령 실행으로 이어질 수 있었다(SERVER-1987 B안에서 별도 차단). - malloc -> calloc: 채워지지 않은 항목이 0으로 남아 기존 nm_device 가드가 의도대로 동작한다. get_physical_stat 의 정확한 계약과 무관하게 안전하다. - get_physical_stat 반환값이 항목 수로 보이면(0 <= ret < count) 그 값으로 루프를 제한한다. 아니면 no-op 이므로 손해가 없다. - count <= 0 이면 빈 dict 반환. 기존에는 count 초기값 -1 로 malloc(음수*size) 경로가 있었다. - 이름이 필드를 가득 채운 경우를 대비해 사용 직전 널 종료를 보장한다. 검증: 대상 장비(whaHP_IA)에서 실제 헤더로 컴파일 확인 cc -c +z -DPSUTIL_VERSION=596 -I _psutil_hpux.c -> 에러 0 Co-Authored-By: Claude Opus 5 (1M context) --- psutil/_psutil_hpux.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/psutil/_psutil_hpux.c b/psutil/_psutil_hpux.c index bcd7178..10ddfcc 100644 --- a/psutil/_psutil_hpux.c +++ b/psutil/_psutil_hpux.c @@ -603,15 +603,35 @@ static PyObject *psutil_net_io_counters(PyObject *self, PyObject *args) { count = val; close_mib(fd); + /* ID_ifNumber counts logical interfaces, which can exceed the number of + physical entries get_physical_stat() actually fills (observed on HP-UX + 11.23 IA: ifNumber=4 but only lan0/lan1/lo0 are filled). Walking up to + ifNumber therefore read entries that were never written, and with + malloc() those held stale heap bytes, so nm_device[0]=='\0' did not skip + them and uninitialised memory leaked out as an interface name. */ + if (count <= 0) { + return PyDict_New(); + } + nmapi_phystat *ifptr; ulen = (unsigned int) count * sizeof(nmapi_phystat); - ifptr = (nmapi_phystat *)malloc(ulen); + ifptr = (nmapi_phystat *)calloc((size_t) count, sizeof(nmapi_phystat)); + if (ifptr == NULL) { + return PyErr_NoMemory(); + } if ((ret = get_physical_stat(ifptr, &ulen)) < 0) { free(ifptr); return NULL; } + /* Prefer the number of entries the call reports over ifNumber. The clamp is + a no-op if ret is not an entry count on this release, while calloc() above + keeps unfilled slots zeroed either way. */ + if (ret >= 0 && ret < count) { + count = ret; + } + int i = 0; PyObject *py_retdict = PyDict_New(); @@ -619,6 +639,9 @@ static PyObject *psutil_net_io_counters(PyObject *self, PyObject *args) { mib_ifEntry *mib; for (; i < count; i++) { + /* A name that fills the field would leave PyDict_SetItemString reading + past it; terminate defensively before the string is used. */ + ifptr[i].nm_device[sizeof(ifptr[i].nm_device) - 1] = '\0'; if(ifptr[i].nm_device[0] == '\0') { continue; }