Skip to content

[CRITICAL BUG] nk_str_insert_text_char and nk_str_insert_str_char has wrong implementation #840

@PavelSharp

Description

@PavelSharp

Introduction

As we know, Nuklear implements its own internal string type (nk_str) along with a set of helper functions.
Most of these functions follow a consistent naming convention:

  • nk_str_*_text_char — means the functions that take a string with a length specified in bytes.
  • nk_str_*_str_char — means the functions that take a null-terminated string, where length is determined automatically using nk_strlen.
  • nk_str_*_text_utf8 — means the functions that take a string with a length specified as the number of code points (runes).
  • nk_str_*_str_utf8 — means the functions that take a null-terminated UTF-8 string, automatically counting runes until an invalid UTF-8 sequence is reached.

Problem

The function nk_str_insert_text_char (and by extension nk_str_insert_str_char) violates this contract.
Despite being part of the _text_char group, it interprets its len parameter as the number of UTF-8 code points, not bytes. Currently, Nuklear incorrectly calls:

NK_API int nk_str_insert_text_char(struct nk_str *str, int pos, const char *text, int len)
{
    return nk_str_insert_text_utf8(str, pos, text, len);
}

This causes out-of-bounds memory access when the input string contains UTF-8 sequences.
This issue is critical, because it directly affects clipboard operations. When nk_textedit_paste() is called, it internally calls nk_str_insert_text_char(), which in turn triggers invalid memory access.
That means pasting any non-ASCII text can cause memory corruption or crashes.

How to fix?

Replace the current implementation(in Nuklear.h for users) to a new one(PR will be ready soon):

NK_API int nk_str_insert_text_char(struct nk_str *str, int pos, const char *text, int len)
{
    nk_str_insert_at_rune(str, pos, text, len);
    return len;
}
NK_API int nk_str_insert_str_char(struct nk_str *str, int pos, const char *text)
{
    return nk_str_insert_text_char(str, pos, text, nk_strlen(text));
}

This ensures that _text_char and _str_char functions comply with their naming contract by treating len as the number of bytes, while _utf8 functions continue to operate in terms of UTF-8 code points.

Practical example

As proof of incorrect behavior, a small program for testing the clipboard is presented below.

Source code
//2025.10.17
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <glad/gl.h>
#include <GLFW/glfw3.h>

#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#define NK_IMPLEMENTATION
#define NK_GLFW_GL3_IMPLEMENTATION
#include "nuklear/nuklear.h"
#include "nuklear/nuklear_glfw_gl3.h"

#define MAX_VERTEX_BUFFER 512 * 1024
#define MAX_ELEMENT_BUFFER 128 * 1024

typedef struct {
    GLFWwindow* window;
    struct nk_context* nk_ctx;
    struct nk_glfw nk_glfw;
    struct nk_text_edit text_edit;
} app_state;


//Greek alphabet. Equivalent to "ΑΒΓ". Or use /utf-8 for msvc
const char* test_string = "\xCE\x91\xCE\x92\xCE\x93";

static inline char* utils_strdup_sized(const char* src, size_t len) {
    if (!src) return NULL;
    char* ret = (char*)malloc(len + 1);//include '\0'
    if (!ret) return NULL;
    ret[len] = '\0';
    return memcpy(ret, src, len);
}

static void glfw_error_callback(int error, const char* desc) {
    printf("GLFW Error %d: %s\n", error, desc);
}

static void app_init_glfw_window(app_state* st, int w, int h) {
    assert(glfwInit());

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    st->window = glfwCreateWindow(w, h, "Clipboard tester", NULL, NULL);
    assert(st->window);

    glfwSetWindowPos(st->window, 100, 100);
    glfwMakeContextCurrent(st->window);
    glfwSetErrorCallback(glfw_error_callback);

    assert(gladLoadGL(glfwGetProcAddress));
}


static void app_nuklear_init(app_state* st) {
    st->nk_ctx = nk_glfw3_init(&st->nk_glfw, st->window, NK_GLFW3_INSTALL_CALLBACKS);
    struct nk_font_atlas* atlas;
    nk_glfw3_font_stash_begin(&st->nk_glfw, &atlas);
    nk_glfw3_font_stash_end(&st->nk_glfw);
}

static void app_set_text(app_state* st, const char* text) {
    struct nk_text_edit* edit = &st->text_edit;
    struct nk_str* str = &st->text_edit.string;
    nk_str_clear(str);
    nk_str_append_str_char(str, text); 
}

static void app_init(app_state* st) {
    app_init_glfw_window(st, 500, 140);
    app_nuklear_init(st);

    nk_textedit_init_default(&st->text_edit);
    app_set_text(st, test_string);
}

static char* utils_nk_str_dup(const struct nk_str* str, int* out_len) {
    *out_len = nk_str_len_char(str);
    if (*out_len==0) return NULL;
    return utils_strdup_sized(nk_str_get_const(str), *out_len);
}

static void app_check_copy_paste(app_state* st) {
    //nk_textedit_text(&st->text_edit, text, strlen(text));
    struct nk_text_edit* edit = &st->text_edit;
    struct nk_str* str = &st->text_edit.string;
    
    struct nk_clipboard* clip = &st->nk_ctx->clip;
    assert(clip && clip->copy && clip->paste && "Required by test");

    int text_len; char* text = utils_nk_str_dup(str, &text_len);

    if (!text_len) { printf("Canceled"); return;} 

    clip->copy(clip->userdata, text, text_len);
    printf("OK. %d bytes was copying to clipboard\n", text_len+1);//include '\0'

    nk_str_clear(str);
    clip->paste(clip->userdata, edit);

    int new_len; char* new_text = utils_nk_str_dup(str, &new_len);

    if (text_len != new_len)
        printf("Fail. Size test error(old=%d, new=%d)\n", text_len, new_len);
    else {
        printf("OK. Size test passed\n");
        int cmpres = strcmp(text, new_text);
        if (cmpres != 0)
            printf("Fail. Text content error(strcmp(old, new)=%d)\n", cmpres);
        else
           printf("OK. Text content passed\n");
    }
    free(text);
    free(new_text);
}

static inline int app_window_open(app_state* st) {
    return !glfwWindowShouldClose(st->window);
}

static void app_ui_frame(app_state* st) {
    struct nk_context* ctx = st->nk_ctx;
    int width, height;
    glfwGetFramebufferSize(st->window, &width, &height);

    nk_glfw3_new_frame(&st->nk_glfw);

    nk_begin(ctx, "MainWindow", nk_rect(5, 20, width - 10, 100), NK_WINDOW_NO_SCROLLBAR);
    {
        const float row_height = 26.0f;
        const float button_width = 150.0f;
        nk_layout_row_template_begin(ctx, row_height);
        nk_layout_row_template_push_dynamic(ctx);
        nk_layout_row_template_push_static(ctx, button_width);
        nk_layout_row_template_end(ctx);

        struct nk_text_edit* edit = &st->text_edit;
        nk_flags flags = nk_edit_buffer(ctx, NK_EDIT_FIELD | NK_EDIT_GOTO_END_ON_ACTIVATE, &st->text_edit, nk_filter_default);

        if (nk_button_label(ctx, "Check Copy&&Paste")) {
            app_check_copy_paste(st);
        }

        nk_layout_row_dynamic(ctx, 20, 1);
        nk_label(ctx, "Feel free to input non-ascii symbols. If you see '?' it's ok", NK_TEXT_ALIGN_LEFT);
        nk_label(ctx, "1 ASCII symbols works fine", NK_TEXT_ALIGN_LEFT);
        nk_label(ctx, "2 Non-ascii symbols just don't work.", NK_TEXT_ALIGN_LEFT);
    }
    nk_end(ctx);

    nk_glfw3_render(&st->nk_glfw, NK_ANTI_ALIASING_ON, MAX_VERTEX_BUFFER, MAX_ELEMENT_BUFFER);
}

static void app_frame(app_state* st) {
    glfwPollEvents();
    int width, height;
    glfwGetFramebufferSize(st->window, &width, &height);

    glViewport(0, 0, width, height);
    glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    app_ui_frame(st);
    glfwSwapBuffers(st->window);
}

static void app_free(app_state* st) {
    nk_glfw3_shutdown(&st->nk_glfw);
    glfwDestroyWindow(st->window);
    glfwTerminate();
}

int main(void) {
    static app_state st = { 0 };
    app_init(&st);
    while (app_window_open(&st)) {
        app_frame(&st);
    }
    app_free(&st);
    return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions