Skip to content
오늘을살자
Go back

lwIP에서 RX pbuf를 넘긴 뒤 가끔 깨진다: tcpip_input ownership을 먼저 정리하자

Edit page

lwIP 포팅에서 RX 경로가 처음에는 멀쩡해 보이다가
부하를 조금 주면 이상해지는 경우가 있다.

이때 checksum, cache, PHY부터 보게 된다.
그런데 RX 드라이버에서 pbuf ownership을 잘못 넘긴 경우도 많다.

오늘 메모는 Ethernet RX 드라이버가 tcpip_input()으로 pbuf를 넘긴 뒤 누가 free하고 누가 재사용해야 하는지다.

결론부터

RX 드라이버가 pbuf를 만들어 lwIP에 넘기는 구조라면
성공과 실패 경로의 ownership을 분리해야 한다.

tcpip_input(p, netif) == ERR_OK
-> lwIP가 pbuf ownership을 가져갔다
-> driver가 다시 pbuf_free() 하면 안 됨

tcpip_input(p, netif) != ERR_OK
-> lwIP가 받지 못했다
-> driver가 pbuf_free() 해야 함

이 규칙 하나가 흐리면
증상은 랜덤 메모리 문제처럼 보인다.

흔한 패턴 1: 성공했는데 driver가 다시 free한다

가장 위험한 실수는 이 형태다.

struct pbuf *p = low_level_rx();

if (p != NULL) {
  tcpip_input(p, &g_netif);
  pbuf_free(p);
}

겉으로는 깔끔해 보인다.
받은 버퍼를 마지막에 정리하는 코드처럼 보이기 때문이다.

하지만 tcpip_input()이 성공했다면
그 pbuf는 이미 lwIP 쪽 큐와 프로토콜 처리 흐름으로 넘어갔다.
driver가 다시 free하면 double free나 use-after-free가 된다.

부하가 낮을 때는 우연히 지나가도,
패킷이 몰리면 TCP payload가 깨지거나 assert가 터진다.

흔한 패턴 2: 실패했는데 free하지 않는다

반대 실수도 있다.

err_t err = tcpip_input(p, &g_netif);

if (err != ERR_OK) {
  log_error(err);
  return;
}

이 경우 tcpip_input()이 큐에 넣지 못한 pbuf가 그대로 남는다.

ERR_MEM이나 mailbox full 상황이 반복되면
작은 누수가 쌓여 PBUF_POOL이 먼저 마른다.

그래서 실패 경로는 명확해야 한다.

err_t err = tcpip_input(p, &g_netif);

if (err != ERR_OK) {
  pbuf_free(p);
}

핵심은 “성공하면 넘겼고, 실패하면 내가 치운다”다.

흔한 패턴 3: DMA RX buffer를 너무 빨리 재사용한다

zero-copy 또는 비슷한 구조에서는 더 조심해야 한다.

RX descriptor가 가리키는 DMA buffer를 pbuf payload로 연결한 뒤,
바로 descriptor를 다시 hardware에 돌려주면 문제가 된다.

DMA buffer -> pbuf payload
tcpip_input(p)
descriptor reused
hardware writes new frame
lwIP still parses old pbuf

이러면 lwIP 입장에서는 같은 pbuf payload가 중간에 바뀐 것처럼 보인다.

증상은 꽤 헷갈린다.

zero-copy를 쓸 거면
pbuf가 해제되는 시점까지 DMA buffer도 재사용하면 안 된다.
그 규칙을 지키기 어렵다면 bring-up 초기에는 복사 방식으로 기준을 만드는 편이 낫다.

관련 버퍼 수명주기는 lwIP pbuf가 가끔 터진다: PBUF_REF/POOL/RAM 수명주기와 zero-copy 함정와 이어진다.

흔한 패턴 4: ISR에서 바로 input 경로를 밀어 넣는다

프로젝트마다 구조는 다르지만,
ISR에서 너무 많은 일을 하면 ownership 디버깅도 어려워진다.

Ethernet IRQ
-> RX descriptor scan
-> pbuf alloc
-> tcpip_input

이 자체가 항상 틀렸다고 말할 수는 없다.
하지만 RTOS 포팅에서는 보통 ISR과 tcpip_thread 사이의 경계를 분명히 두는 편이 낫다.

특히 아래가 섞이면 문제가 더 잘 숨는다.

ISR에서는 이벤트만 깨우고,
RX task 또는 driver task에서 pbuf를 만들어 tcpip_input() 결과를 처리하면 로그가 훨씬 읽기 쉽다.

컨텍스트 분리 흐름은 lwIP에서 ISR에서 바로 보내면 가끔 터진다: tcpip_thread로 넘기는 패턴 정리와 같이 보면 좋다.

pbuf ref count를 로그로 보면 빨라진다

이 문제는 마지막 crash 지점만 보면 원인이 멀리 있어 보인다.

나는 RX input 경로에 잠깐 아래 로그를 넣고 본다.

rx p=0x20012000 type=POOL len=60 tot_len=60 ref=1
tcpip_input ret=ERR_OK
owner=lwip

실패 경로라면 이렇게 남긴다.

rx p=0x20012400 type=POOL len=1514 tot_len=1514 ref=1
tcpip_input ret=ERR_MEM
owner=driver-free

이 정도만 있어도
“누가 마지막에 free했는가”를 추적하기 쉬워진다.

가능하면 아래 항목을 같이 남긴다.

  1. RX descriptor index
  2. pbuf 주소와 payload 주소
  3. len, tot_len, ref
  4. tcpip_input() 반환값
  5. 성공/실패별 free 여부
  6. DMA buffer 재사용 시각

구현 쪽에서 무난한 형태

복사 기반 RX라면 이런 모양이 이해하기 쉽다.

struct pbuf *p = low_level_rx_copy();

if (p == NULL) {
  return;
}

err_t err = tcpip_input(p, &g_netif);

if (err != ERR_OK) {
  pbuf_free(p);
}

성공 경로에는 pbuf_free(p)가 없다.

zero-copy 기반이라면 pbuf custom free callback이나 driver buffer pool 회수 지점까지 같이 설계해야 한다.
그냥 descriptor를 먼저 재사용하면 ownership 규칙이 깨진다.

빠른 체크리스트

  1. tcpip_input() 성공 뒤 driver가 pbuf_free()를 다시 호출하지 않는지 확인
  2. tcpip_input() 실패 경로에서는 driver가 반드시 pbuf_free()하는지 확인
  3. DMA buffer를 pbuf payload로 넘긴 뒤 너무 빨리 RX descriptor에 돌려주지 않는지 확인
  4. ISR, RX task, tcpip_thread 중 어느 컨텍스트가 pbuf ownership을 갖는지 정리
  5. pbuf 주소, ref count, input 반환값, free 여부를 같은 로그 라인에 남기는지 확인

함께 보면 좋은 글

한 줄 요약

lwIP RX 경로에서 tcpip_input()으로 pbuf를 넘긴 뒤 문제가 난다면, 성공 시 ownership은 lwIP로 넘어가고 실패 시 driver가 free한다는 기준부터 코드와 로그에 분명히 두는 편이 빠르다.

추천 키워드

lwIP, TCPIP, pbuf, tcpip_input, Ethernet MAC, DMA


DevBJ | 오늘을살자, Log Today


Edit page
Share this post on:

Next Post
DoIP에서 NRC 0x22가 뜬다: 조건 미충족을 통신 timeout처럼 보지 말자