[PATCH] tools api io: Return high-bit bytes as unsigned characters

From: Wojciech Szpucha

Date: Thu Nov 12 2026 - 12:13:23 EST


From: Wojciech Szpucha <szpuszi@xxxxxxxxx>

On signed-char builds, io__get_char() returns negative values for bytes
0x80-0xff. This makes filename__read_str() silently truncate non-ASCII
input in standalone libapi builds.

Cast the byte to unsigned char before returning it as int. Add regression
tests compiled with -fsigned-char, since perf defaults to -funsigned-char
and already avoids the issue.

Fixes: e95770af4c4a ("tools api: Add a lightweight buffered reading api")
Signed-off-by: Wojciech Szpucha <szpuszi@xxxxxxxxx>
---
Tested on Ubuntu 24.04 x86_64, GCC 13.3.0: standalone libapi reads 0/4
bytes before the fix and 4/4 afterwards. The API IO regression fails
before and passes after. libapi and perf build successfully; checkpatch
passes. Full perf suite and kernel builds were not run.

https://github.com/szpuszi/linux-perf-io-test/actions/runs/43257707229

tools/lib/api/io.h | 2 +-
tools/perf/tests/Build | 1 +
tools/perf/tests/api-io.c | 33 ++++++++++++++++++++++++++++++++-
3 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/tools/lib/api/io.h b/tools/lib/api/io.h
index 1731996b..25e2008d 100644
--- a/tools/lib/api/io.h
+++ b/tools/lib/api/io.h
@@ -91,7 +91,7 @@ static inline int io__get_char(struct io *io)
if (ret)
return ret;
}
- return *io->data++;
+ return (unsigned char)*io->data++;
}

/* Read a hexadecimal value with no 0x prefix into the out argument hex. If the
diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build
index 58858f3d..c3ba1fca 100644
--- a/tools/perf/tests/Build
+++ b/tools/perf/tests/Build
@@ -79,6 +79,7 @@ perf-test-$(CONFIG_DWARF_UNWIND) += dwarf-unwind.o
endif

CFLAGS_dwarf-unwind.o += -fno-optimize-sibling-calls
+CFLAGS_api-io.o += -fsigned-char

perf-test-y += workloads/

diff --git a/tools/perf/tests/api-io.c b/tools/perf/tests/api-io.c
index 0ba3d5cc..f283712e 100644
--- a/tools/perf/tests/api-io.c
+++ b/tools/perf/tests/api-io.c
@@ -98,7 +98,7 @@ static int do_test_get_char(const char *test_string,
size_t buf_size)
for (i = 0; i < strlen(test_string); i++) {
ch = io__get_char(&io);

- EXPECT_EQUAL(ch, test_string[i]);
+ EXPECT_EQUAL(ch, (unsigned char)test_string[i]);
EXPECT_EQUAL(io.eof, false);
}
ch = io__get_char(&io);
@@ -118,6 +118,7 @@ static int test_get_char(void)
"12345678abcdef90",
"a\nb\nc\nd\n",
"\a\b\t\v\f\r",
+ "\x7f\x80\xfe\xff",
};
for (i = 0; i <= 10; i++) {
for (j = 0; j < ARRAY_SIZE(test_strings); j++) {
@@ -290,6 +291,34 @@ static int test_get_dec(void)
return ret;
}

+static int test_get_line_non_ascii(void)
+{
+ static const char test_string[] = "a\xc3\xa9\xff\nnext\n";
+ char path[PATH_MAX];
+ struct io io;
+ char *line = NULL;
+ size_t line_len = 0;
+ int len, ret = 0;
+
+ if (setup_test(path, test_string, 2, &io))
+ return -1;
+
+ len = io__getline(&io, &line, &line_len);
+ EXPECT_EQUAL(len, 5);
+ EXPECT_EQUAL((int)line_len, 5);
+ EXPECT_EQUAL((int)strlen(line), 5);
+ if (line_len == 5)
+ EXPECT_EQUAL(memcmp(line, test_string, 5), 0);
+ len = io__getline(&io, &line, &line_len);
+ EXPECT_EQUAL(len, 5);
+ EXPECT_EQUAL((int)line_len, 5);
+ EXPECT_EQUAL(strcmp(line, "next\n"), 0);
+
+ free(line);
+ cleanup_test(path, &io);
+ return ret;
+}
+
static int test_get_line(void)
{
char path[PATH_MAX];
@@ -337,6 +366,8 @@ static int test__api_io(struct test_suite *test
__maybe_unused,
ret = TEST_FAIL;
if (test_get_line())
ret = TEST_FAIL;
+ if (test_get_line_non_ascii())
+ ret = TEST_FAIL;
return ret;
}


base-commit: 9d66f26fefacebfeed15de31c1123451a17ff119