-
Notifications
You must be signed in to change notification settings - Fork 648
Description
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 usingnk_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;
}