open5gs/lib/core/ogs-tlv.c
Sukchan Lee 35ce855e32 core/tlv, smf: Harden TLV parsing and validate Bearer Context in CSR
Two issues (#4277, #4278) reported crashes caused by malformed or
unexpected inputs.

In the TLV parser, several ogs_assert() checks could be triggered by
malformed TLV blocks, resulting in process termination. These checks
are replaced with proper error handling: the parser now logs the error,
limits hexdump size, frees allocated TLVs, and returns NULL instead of
aborting.

In the SMF S5-C Create Session Request handler, additional validation
is introduced for Bearer Context handling. The implementation now
rejects requests containing multiple Bearer Contexts, missing mandatory
fields (EBI or Bearer QoS), duplicate EBI values, or invalid TEID/IP
information. Several ogs_assert() calls that could be triggered by
malformed messages are also replaced with explicit error handling.

These changes prevent crashes caused by malformed TLV blocks or
unexpected Bearer Context structures and ensure the SMF rejects such
requests gracefully.

Issues: #4277, #4278
2026-03-06 10:05:24 +09:00

549 lines
13 KiB
C

/*
* Copyright (C) 2019,2026 by Sukchan Lee <acetcom@gmail.com>
* Copyright (C) 2022 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
*
* This file is part of Open5GS.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ogs-core.h"
#undef OGS_LOG_DOMAIN
#define OGS_LOG_DOMAIN __ogs_tlv_domain
static OGS_POOL(pool, ogs_tlv_t);
/* ogs_tlv_t common functions */
ogs_tlv_t *ogs_tlv_get(void)
{
ogs_tlv_t *tlv = NULL;
/* get tlv node from node pool */
ogs_pool_alloc(&pool, &tlv);
/* check for error */
if (!tlv) {
ogs_error("ogs_tlv_get() failed");
return NULL;
}
/* initialize tlv node */
memset(tlv, 0, sizeof(ogs_tlv_t));
return tlv;
}
void ogs_tlv_free(ogs_tlv_t *tlv)
{
/* free tlv node to the node pool */
ogs_pool_free(&pool, tlv);
}
void ogs_tlv_init(void)
{
ogs_pool_init(&pool, ogs_core()->tlv.pool);
}
void ogs_tlv_final(void)
{
ogs_pool_final(&pool);
}
uint32_t ogs_tlv_pool_avail(void)
{
return ogs_pool_avail(&pool);
}
void ogs_tlv_free_all(ogs_tlv_t *root)
{
/* free all tlv node to the node pool */
ogs_tlv_t *iter = root;
ogs_tlv_t *next = NULL;
while (iter) {
if(iter->embedded != NULL) {
ogs_tlv_free_all(iter->embedded);
}
next = iter->next;
ogs_tlv_free(iter);
iter = next;
}
}
uint8_t ogs_tlv_value_8(ogs_tlv_t *tlv)
{
return (*((uint8_t*)(tlv->value)));
}
uint16_t ogs_tlv_value_16(ogs_tlv_t *tlv)
{
uint16_t u_16;
uint8_t *v = tlv->value;
u_16 = ((v[0] << 8) & 0xff00) |
((v[1] ) & 0x00ff);
return u_16;
}
uint32_t ogs_tlv_value_32(ogs_tlv_t *tlv)
{
uint32_t u_32;
uint8_t *v = tlv->value;
u_32 = ((v[0] << 24) & 0xff000000) |
((v[1] << 16) & 0x00ff0000) |
((v[2] << 8) & 0x0000ff00) |
((v[3] ) & 0x000000ff);
return u_32;
}
uint32_t ogs_tlv_calc_length(ogs_tlv_t *tlv)
{
ogs_tlv_t *iter = tlv;
uint32_t length = 0;
while(iter) {
/* this is length for type field */
switch(iter->mode) {
case OGS_TLV_MODE_T1_L1:
length += 2;
break;
case OGS_TLV_MODE_T1_L2:
length += 3;
break;
case OGS_TLV_MODE_T1_L2_I1:
case OGS_TLV_MODE_T2_L2:
length += 4;
break;
case OGS_TLV_MODE_T1:
length += 1;
break;
default:
ogs_assert_if_reached();
break;
}
/* this is length for type field */
if(iter->embedded != NULL) {
iter->length = ogs_tlv_calc_length(iter->embedded);
}
/* this is length for value field */
length += iter->length;
iter = iter->next;
}
return length;
}
uint32_t ogs_tlv_calc_count(ogs_tlv_t *tlv)
{
ogs_tlv_t *iter = tlv;
uint32_t count = 0;
while(iter) {
if(iter->embedded != NULL) {
count += ogs_tlv_calc_count(iter->embedded);
} else {
count++;
}
iter = iter->next;
}
return count;
}
static uint8_t *tlv_put_type(uint32_t type, uint8_t *pos, uint8_t mode)
{
switch(mode) {
case OGS_TLV_MODE_T1_L1:
case OGS_TLV_MODE_T1_L2:
case OGS_TLV_MODE_T1_L2_I1:
case OGS_TLV_MODE_T1:
*(pos++) = type & 0xFF;
break;
case OGS_TLV_MODE_T2_L2:
*(pos++) = (type >> 8) & 0xFF;
*(pos++) = type & 0xFF;
break;
default:
ogs_assert_if_reached();
break;
}
return pos;
}
static uint8_t *tlv_put_length(uint32_t length, uint8_t *pos, uint8_t mode)
{
switch(mode) {
case OGS_TLV_MODE_T1_L1:
*(pos++) = length & 0xFF;
break;
case OGS_TLV_MODE_T1_L2:
case OGS_TLV_MODE_T1_L2_I1:
case OGS_TLV_MODE_T2_L2:
*(pos++) = (length >> 8) & 0xFF;
*(pos++) = length & 0xFF;
break;
case OGS_TLV_MODE_T1:
break;
default:
ogs_assert_if_reached();
break;
}
return pos;
}
static uint8_t *tlv_put_instance(uint8_t instance, uint8_t *pos, uint8_t mode)
{
switch(mode) {
case OGS_TLV_MODE_T1_L2_I1:
*(pos++) = instance & 0xFF;
break;
default:
break;
}
return pos;
}
uint8_t *tlv_get_element(ogs_tlv_t *tlv, uint8_t *blk, uint8_t mode)
{
uint8_t *pos = blk;
tlv->mode = mode;
switch(mode) {
case OGS_TLV_MODE_T1_L1:
tlv->type = *(pos++);
tlv->length = *(pos++);
break;
case OGS_TLV_MODE_T1_L2:
tlv->type = *(pos++);
tlv->length = *(pos++) << 8;
tlv->length += *(pos++);
break;
case OGS_TLV_MODE_T1_L2_I1:
tlv->type = *(pos++);
tlv->length = *(pos++) << 8;
tlv->length += *(pos++);
tlv->instance = *(pos++) & 0b00001111;
break;
case OGS_TLV_MODE_T2_L2:
tlv->type = *(pos++) << 8;
tlv->type += *(pos++);
tlv->length = *(pos++) << 8;
tlv->length += *(pos++);
break;
case OGS_TLV_MODE_T1:
tlv->type = *(pos++);
tlv->length = 0;
break;
default:
ogs_assert_if_reached();
break;
}
tlv->value = pos;
return (pos + ogs_tlv_length(tlv));
}
uint8_t *tlv_get_element_fixed(ogs_tlv_t *tlv, uint8_t *blk, uint8_t mode, uint32_t fixed_length)
{
uint8_t *pos = blk;
switch(mode) {
case OGS_TLV_MODE_T1:
tlv->type = *(pos++);
tlv->length = fixed_length;
break;
default:
ogs_assert_if_reached();
break;
}
tlv->value = pos;
return (pos + ogs_tlv_length(tlv));
}
static void tlv_alloc_buff_to_tlv(
ogs_tlv_t *head, uint8_t *buff, uint32_t buff_len)
{
head->buff_allocated = true;
head->buff_len = buff_len;
head->buff_ptr = buff;
head->buff = buff;
}
ogs_tlv_t *ogs_tlv_find_root(ogs_tlv_t *tlv)
{
ogs_tlv_t *head = tlv->head;
ogs_tlv_t *parent;
parent = head->parent;
while(parent) {
head = parent->head;
parent = head->parent;
}
return head;
}
ogs_tlv_t *ogs_tlv_add(ogs_tlv_t *head, uint8_t mode,
uint32_t type, uint32_t length, uint8_t instance, void *value)
{
ogs_tlv_t *curr = head;
ogs_tlv_t *new = NULL;
new = ogs_tlv_get();
ogs_assert(new);
if(length != 0)
ogs_assert(value);
new->mode = mode;
new->type = type;
new->length = length;
new->instance = instance;
new->value = value;
if (head != NULL && head->buff_allocated == true) {
ogs_assert((head->buff_ptr - head->buff + length) < head->buff_len);
memcpy(head->buff_ptr, value, length);
new->value = head->buff_ptr;
head->buff_ptr += length;
}
if(curr == NULL) {
new->head = new;
new->tail = new;
} else {
head = head->head; /* in case head is not head */
new->head = head;
head->tail->next = new;
head->tail = new;
}
return new;
}
ogs_tlv_t *ogs_tlv_copy(void *buff, uint32_t buff_len, uint8_t mode,
uint32_t type, uint32_t length, uint8_t instance, void *value)
{
ogs_tlv_t *new = NULL;
new = ogs_tlv_get();
ogs_assert(new);
new->mode = mode;
new->type = type;
new->length = length;
new->instance = instance;
new->value = value;
new->head = new->tail = new;
tlv_alloc_buff_to_tlv(new, buff, buff_len);
memcpy(new->buff_ptr, value, length);
new->value = new->buff_ptr;
new->buff_ptr += length;
return new;
}
ogs_tlv_t *ogs_tlv_embed(ogs_tlv_t *parent, uint8_t mode,
uint32_t type, uint32_t length, uint8_t instance, void *value)
{
ogs_tlv_t *new = NULL, *root = NULL;
ogs_assert(parent);
new = ogs_tlv_get();
ogs_assert(new);
new->mode = mode;
new->type = type;
new->length = length;
new->instance = instance;
new->value = value;
root = ogs_tlv_find_root(parent);
if(root->buff_allocated == true) {
ogs_assert((root->buff_ptr - root->buff + length) < root->buff_len);
memcpy(root->buff_ptr, value, length);
new->value = root->buff_ptr;
root->buff_ptr += length;
}
if(parent->embedded == NULL) {
parent->embedded = new->head = new->tail = new;
new->parent = parent;
} else {
new->head = parent->embedded;
parent->embedded->tail->next = new;
parent->embedded->tail = new;
}
return new;
}
uint32_t ogs_tlv_render(ogs_tlv_t *root, void *data, uint32_t length)
{
ogs_tlv_t *curr = root;
uint8_t *pos = data;
uint8_t *blk = data;
uint32_t embedded_len = 0;
while(curr) {
pos = tlv_put_type(curr->type, pos, curr->mode);
if(curr->embedded == NULL) {
pos = tlv_put_length(curr->length, pos, curr->mode);
pos = tlv_put_instance(curr->instance, pos, curr->mode);
if ((pos - blk) + ogs_tlv_length(curr) > length)
ogs_assert_if_reached();
memcpy((char*)pos, (char*)curr->value, curr->length);
pos += curr->length;
} else {
embedded_len = ogs_tlv_calc_length(curr->embedded);
pos = tlv_put_length(embedded_len, pos, curr->mode);
pos = tlv_put_instance(curr->instance, pos, curr->mode);
ogs_tlv_render(curr->embedded,
pos, length - (uint32_t)(pos-blk));
pos += embedded_len;
}
curr = curr->next;
}
return (pos - blk);
}
/* ogs_tlv_t parsing functions */
ogs_tlv_t *ogs_tlv_parse_block(uint32_t length, void *data, uint8_t mode)
{
uint8_t *pos = data;
uint8_t *blk = data;
ogs_tlv_t *root = NULL;
ogs_tlv_t *prev = NULL;
ogs_tlv_t *curr = NULL;
root = curr = ogs_tlv_get();
if (!curr) {
ogs_error("ogs_tlv_parse_block() failed[LEN:%d,MODE:%d] - no tlv",
length, mode);
/*
* Limit hexdump size to avoid excessive logging when handling malformed
* or intentionally crafted messages. This prevents log flooding and
* secondary DoS effects while still providing enough data for debugging.
*/
ogs_log_hexdump(OGS_LOG_ERROR, data, ogs_min(length, 512));
return NULL;
}
pos = tlv_get_element(curr, pos, mode);
if (!pos) {
ogs_error("ogs_tlv_parse_block() failed[LEN:%u,MODE:%u] - parse error",
length, mode);
/*
* Limit hexdump size to avoid excessive logging when handling malformed
* or intentionally crafted messages. This prevents log flooding and
* secondary DoS effects while still providing enough data for debugging.
*/
ogs_log_hexdump(OGS_LOG_ERROR, data, ogs_min(length, 512));
ogs_tlv_free_all(root);
return NULL;
}
while(pos - blk < length) {
prev = curr;
curr = ogs_tlv_get();
if (!curr) {
ogs_error("ogs_tlv_parse_block() failed[LEN:%d,MODE:%d]",
length, mode);
/*
* Limit hexdump size to avoid excessive logging when handling malformed
* or intentionally crafted messages. This prevents log flooding and
* secondary DoS effects while still providing enough data for debugging.
*/
ogs_log_hexdump(OGS_LOG_ERROR, data, ogs_min(length, 512));
ogs_tlv_free_all(root);
return NULL;
}
prev->next = curr;
pos = tlv_get_element(curr, pos, mode);
if (!pos) {
ogs_error("ogs_tlv_parse_block() failed[LEN:%d,MODE:%d]",
length, mode);
/*
* Limit hexdump size to avoid excessive logging when handling malformed
* or intentionally crafted messages. This prevents log flooding and
* secondary DoS effects while still providing enough data for debugging.
*/
ogs_log_hexdump(OGS_LOG_ERROR, data, ogs_min(length, 512));
ogs_tlv_free_all(root);
return NULL;
}
}
if (length != (pos - blk)) {
ogs_error("ogs_tlv_parse_block() failed[LEN:%d,MODE:%d]", length, mode);
ogs_error("POS[%p] BLK[%p] POS-BLK[%d]", pos, blk, (int)(pos - blk));
ogs_log_hexdump(OGS_LOG_ERROR, data, ogs_min(length, 512));
ogs_tlv_free_all(root);
return NULL;
}
return root;
}
ogs_tlv_t *ogs_tlv_parse_embedded_block(ogs_tlv_t *tlv, uint8_t mode)
{
tlv->embedded = ogs_tlv_parse_block(tlv->length, tlv->value, mode);
return tlv->embedded;
}
/* tlv operation-related function */
ogs_tlv_t *ogs_tlv_find(ogs_tlv_t *root, uint32_t type)
{
ogs_tlv_t *iter = root, *embed = NULL;
while(iter) {
if(iter->type == type) {
return iter;
}
if(iter->embedded != NULL) {
embed = ogs_tlv_find(iter->embedded, type);
if(embed != NULL) {
return embed;
}
}
iter = iter->next;
}
/* tlv for the designated type doesn't exist */
return NULL;
}