core/sbi: fix hex conversion UB and validate GUAMI AMF IDs
Some checks failed
CIFuzz / Fuzzing (push) Has been cancelled
Meson Continuous Integration / Build and Test on MacOS Latest (push) Has been cancelled
Meson Continuous Integration / Build and Test on Ubuntu Latest (push) Has been cancelled

Fix the negative left shift reported by sbi_request_fuzz in
ogs_ascii_to_hex(). Mask converted digits to a nibble and pass
unsigned char values to ctype functions.

Log missing buffers, invalid lengths and non-hex characters in the
checked decoder. Delegate the string wrapper to it without duplicate
logs or an additional header dependency.

Require exactly six hex digits for AMF IDs and propagate conversion
failures through GUAMI discovery, NF profiles and AMF/UDM/SMF/PCF
handlers, with contextual error logs.

Validate UDM registration GUAMI before replacing the stored callback
URI. Include an INVALID_MANDATORY_INFORMATION N1 reject when SMF
rejects an invalid GUAMI, matching the existing No Guami path.
This commit is contained in:
Sukchan Lee 2026-10-16 21:18:05 +09:00
parent f5c7025456
commit db5935b334
11 changed files with 118 additions and 24 deletions

View file

@ -35,9 +35,15 @@ int ogs_ascii_to_hex(char *in, int in_len, void *out, int out_len)
uint8_t *out_p = out;
while(i < in_len && j < out_len) {
if (!isspace(in[i])) {
hex = isdigit(in[i]) ? in[i] - '0' :
islower(in[i]) ? in[i] - 'a' + 10 : in[i] - 'A' + 10;
if (!isspace((unsigned char)in[i])) {
hex = isdigit((unsigned char)in[i]) ? in[i] - '0' :
islower((unsigned char)in[i]) ?
in[i] - 'a' + 10 : in[i] - 'A' + 10;
/*
* Non-hex input may produce a negative value. Mask it before
* shifting; input validation remains the checked API's job.
*/
hex &= 0x0f;
if ((k & 0x1) == 0) {
out_p[j] = (hex << 4);
} else {
@ -57,18 +63,34 @@ int ogs_ascii_to_hex_checked(
{
int i;
if (!in || !out)
if (!in) {
ogs_error("Missing hex input");
return OGS_ERROR;
}
if (!out) {
ogs_error("Missing hex output buffer");
return OGS_ERROR;
}
if (in_len < 0 || out_len < 0 || out_len > INT_MAX / 2 ||
in_len != out_len * 2)
if (in_len < 0 || out_len < 0 || out_len > INT_MAX / 2) {
ogs_error("Invalid hex conversion length [input:%d output:%d]",
in_len, out_len);
return OGS_ERROR;
}
if (in_len != out_len * 2) {
ogs_error("Invalid hex string length [%d], expected [%d]",
in_len, out_len * 2);
return OGS_ERROR;
}
for (i = 0; i < in_len; i++) {
if (!((in[i] >= '0' && in[i] <= '9') ||
(in[i] >= 'a' && in[i] <= 'f') ||
(in[i] >= 'A' && in[i] <= 'F')))
(in[i] >= 'A' && in[i] <= 'F'))) {
ogs_error("Invalid hex character [0x%02x] at offset [%d]",
(unsigned char)in[i], i);
return OGS_ERROR;
}
}
ogs_ascii_to_hex((char *)in, in_len, out, out_len);

View file

@ -28,25 +28,41 @@
extern "C" {
#endif
/*
* Skip whitespace and convert up to out_len bytes without validating hex.
* Return the number of complete bytes. Short/odd input may leave partial
* output; excess input is ignored. out_len is capacity, not an exact size.
* Use ogs_ascii_to_hex_checked() for received, fixed-size hex fields.
*/
int ogs_ascii_to_hex(char *in, int in_len, void *out, int out_len);
/*
* Convert exactly out_len * 2 hexadecimal characters.
* Return OGS_OK on success. On error, leave out unchanged.
* Require exactly out_len * 2 hex digits (0-9, a-f, A-F), without whitespace.
* Return OGS_OK, or log an error and return OGS_ERROR without changing out.
* Check the result before using the output; out_len is the exact decoded size.
*
* Added for AUSF vector validation after issue #4734
* (opened 2026-08-08): https://github.com/open5gs/open5gs/issues/4734
*/
int ogs_ascii_to_hex_checked(
const char *in, int in_len, void *out, int out_len);
/*
* Permissive string wrapper; returns out without reporting invalid input.
* Use ogs_hex_from_string_checked() for received, fixed-size hex fields.
*/
static ogs_inline void *ogs_hex_from_string(
const char *str, void *out, int out_len)
{
ogs_ascii_to_hex((char*)str, strlen(str), out, out_len);
return out;
}
/*
* String wrapper for ogs_ascii_to_hex_checked(); check the return value.
*/
static ogs_inline int ogs_hex_from_string_checked(
const char *str, void *out, int out_len)
{
if (!str)
return OGS_ERROR;
return ogs_ascii_to_hex_checked(str, strlen(str), out, out_len);
return ogs_ascii_to_hex_checked(
str, str ? strlen(str) : 0, out, out_len);
}
void *ogs_hex_to_ascii(const void *in, int in_len, void *out, int out_len);

View file

@ -246,7 +246,10 @@ ogs_amf_id_t *ogs_amf_id_from_string(ogs_amf_id_t *amf_id, const char *hex)
ogs_assert(amf_id);
ogs_assert(hex);
ogs_hex_from_string(hex, hexbuf, sizeof(hexbuf));
if (ogs_hex_from_string_checked(hex, hexbuf, sizeof(hexbuf)) != OGS_OK) {
ogs_error("Invalid AMF ID [%s]", hex);
return NULL;
}
amf_id->region = hexbuf[0];
amf_id->set1 = hexbuf[1];

View file

@ -287,6 +287,7 @@ typedef struct ogs_guami_s {
uint32_t ogs_amf_id_hexdump(const ogs_amf_id_t *amf_id);
/* Return NULL for malformed hex without changing amf_id. */
ogs_amf_id_t *ogs_amf_id_from_string(ogs_amf_id_t *amf_id, const char *hex);
char *ogs_amf_id_to_string(const ogs_amf_id_t *amf_id);

View file

@ -1345,7 +1345,10 @@ bool ogs_sbi_parse_guami(ogs_guami_t *guami, OpenAPI_guami_t *Guami)
ogs_assert(Guami->amf_id);
ogs_assert(Guami->plmn_id);
ogs_amf_id_from_string(&guami->amf_id, Guami->amf_id);
if (!ogs_amf_id_from_string(&guami->amf_id, Guami->amf_id)) {
ogs_error("Invalid GUAMI AMF ID [%s]", Guami->amf_id);
return false;
}
ogs_sbi_parse_plmn_id_nid(&guami->plmn_id, Guami->plmn_id);
return true;

View file

@ -4158,7 +4158,13 @@ int ogs_sbi_discovery_option_parse_guami(
return OGS_ERROR;
}
ogs_sbi_parse_guami(&parsed_guami, Guami);
if (!ogs_sbi_parse_guami(&parsed_guami, Guami)) {
ogs_error("Invalid GUAMI discovery parameter [%s]", guami);
OpenAPI_guami_free(Guami);
cJSON_Delete(guamItem);
ogs_free(v);
return OGS_ERROR;
}
ogs_sbi_discovery_option_set_guami(discovery_option, &parsed_guami);
OpenAPI_guami_free(Guami);
cJSON_Delete(guamItem);

View file

@ -860,9 +860,14 @@ static bool handle_amf_info(
GuamiAmfInfoItem->plmn_id->mnc &&
GuamiAmfInfoItem->plmn_id->mcc) {
ogs_sbi_parse_guami(
if (!ogs_sbi_parse_guami(
&nf_info->amf.guami[nf_info->amf.num_of_guami],
GuamiAmfInfoItem);
GuamiAmfInfoItem)) {
ogs_error("[%s] Invalid GUAMI in AMF NF profile",
nf_instance->id);
rv = false;
break;
}
nf_info->amf.num_of_guami++;
}
}

View file

@ -2258,7 +2258,11 @@ static bool amf_namf_comm_parse_guti(ogs_nas_5gs_guti_t *guti, char *ue_context_
memset(&plmn_id, 0, sizeof(plmn_id));
ogs_sbi_parse_plmn_id(&plmn_id, &Plmn_id);
ogs_nas_from_plmn_id(&guti->nas_plmn_id, &plmn_id);
ogs_amf_id_from_string(&guti->amf_id, amf_id_string);
if (!ogs_amf_id_from_string(&guti->amf_id, amf_id_string)) {
ogs_error("Invalid AMF ID [%s] in UE context ID [%s]",
amf_id_string, ue_context_id);
return false;
}
guti->m_tmsi = (u_int32_t)strtol(tmsi_string, NULL, 16);
return true;

View file

@ -235,7 +235,14 @@ bool pcf_npcf_am_policy_control_handle_create(pcf_ue_am_t *pcf_ue_am,
Guami = PolicyAssociationRequest->guami;
if (Guami && Guami->amf_id &&
Guami->plmn_id && Guami->plmn_id->mnc && Guami->plmn_id->mcc) {
ogs_sbi_parse_guami(&pcf_ue_am->guami, PolicyAssociationRequest->guami);
if (!ogs_sbi_parse_guami(
&pcf_ue_am->guami, PolicyAssociationRequest->guami)) {
ogs_error("[%s] Invalid GUAMI", pcf_ue_am->supi);
ogs_assert(true ==
ogs_sbi_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST,
message, "Invalid GUAMI", pcf_ue_am->supi, NULL));
return false;
}
}
OpenAPI_list_for_each(PolicyAssociationRequest->allowed_snssais, node) {

View file

@ -422,7 +422,15 @@ bool smf_nsmf_handle_create_sm_context(
ogs_assert(sess->amf_nf_id);
ogs_assert(SmContextCreateData->guami);
ogs_sbi_parse_guami(&sess->guami, SmContextCreateData->guami);
if (!ogs_sbi_parse_guami(&sess->guami, SmContextCreateData->guami)) {
ogs_error("[%s:%d] Invalid GUAMI", smf_ue->supi, sess->psi);
n1smbuf = gsm_build_pdu_session_establishment_reject(sess,
OGS_5GSM_CAUSE_INVALID_MANDATORY_INFORMATION);
smf_sbi_send_sm_context_create_error(stream,
OGS_SBI_HTTP_STATUS_BAD_REQUEST, OGS_SBI_APP_ERRNO_NULL,
"Invalid GUAMI", smf_ue->supi, n1smbuf);
return false;
}
if (SmContextCreateData->pcf_id) {
if (sess->pcf_id) ogs_free(sess->pcf_id);
@ -1618,7 +1626,14 @@ bool smf_nsmf_handle_create_data_in_hsmf(
sess->amf_nf_id = ogs_strdup(PduSessionCreateData->amf_nf_id);
ogs_assert(sess->amf_nf_id);
ogs_sbi_parse_guami(&sess->guami, PduSessionCreateData->guami);
if (!ogs_sbi_parse_guami(&sess->guami, PduSessionCreateData->guami)) {
ogs_error("[%s:%d] Invalid GUAMI", smf_ue->supi, sess->psi);
smf_sbi_send_pdu_session_create_error(stream,
OGS_SBI_HTTP_STATUS_BAD_REQUEST, OGS_SBI_APP_ERRNO_NULL,
OGS_5GSM_CAUSE_INVALID_MANDATORY_INFORMATION,
"Invalid GUAMI", smf_ue->supi, NULL);
return false;
}
sess->s_nssai.sst = sNssai->sst;
sess->s_nssai.sd = ogs_s_nssai_sd_from_string(sNssai->sd);

View file

@ -337,14 +337,20 @@ bool udm_nudm_uecm_handle_amf_registration(
return false;
}
if (!ogs_sbi_parse_guami(&udm_ue->guami, Guami)) {
ogs_error("[%s] Invalid GUAMI", udm_ue->supi);
ogs_assert(true ==
ogs_sbi_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST,
message, "Invalid GUAMI", udm_ue->supi, NULL));
return false;
}
if (udm_ue->dereg_callback_uri)
ogs_free(udm_ue->dereg_callback_uri);
udm_ue->dereg_callback_uri = ogs_strdup(
Amf3GppAccessRegistration->dereg_callback_uri);
ogs_assert(udm_ue->dereg_callback_uri);
ogs_sbi_parse_guami(&udm_ue->guami, Guami);
udm_ue->rat_type = Amf3GppAccessRegistration->rat_type;
udm_ue->amf_3gpp_access_registration =
@ -441,7 +447,13 @@ bool udm_nudm_uecm_handle_amf_registration_update(
* GUAMI. If so, the UDM shall set the PurgeFlag. The UDM responds with
* "204 No Content".
* 2b. Otherwise the UDM responds with "403 Forbidden". */
ogs_sbi_parse_guami(&recv_guami, Guami);
if (!ogs_sbi_parse_guami(&recv_guami, Guami)) {
ogs_error("[%s] Invalid GUAMI", udm_ue->supi);
ogs_assert(true ==
ogs_sbi_server_send_error(stream, OGS_SBI_HTTP_STATUS_BAD_REQUEST,
message, "Invalid GUAMI", udm_ue->supi, NULL));
return false;
}
if (memcmp(&recv_guami, &udm_ue->guami, sizeof(recv_guami)) != 0) {
ogs_error("[%s] Guami mismatch", udm_ue->supi);
/*