From ee2c7875d80602f0687e9a2f2552180d3aa6d7a3 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 11 Mar 2026 15:49:15 +0300 Subject: [PATCH] checker: SIGSEGV termination with os write_struct() and read_struct() when in separate v files (fixes #11285) --- vlib/os/file.c.v | 24 +++++--- vlib/v/checker/check_types.v | 56 +++++++++++++++++++ vlib/v/checker/fn.v | 22 ++++++++ .../os_file_raw_io_unsupported_type_err.out | 27 +++++++++ .../os_file_raw_io_unsupported_type_err.vv | 19 +++++++ 5 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.out create mode 100644 vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv diff --git a/vlib/os/file.c.v b/vlib/os/file.c.v index d3ad04a43..201415a5f 100644 --- a/vlib/os/file.c.v +++ b/vlib/os/file.c.v @@ -503,7 +503,8 @@ fn error_size_of_type_0() IError { return &SizeOfTypeIs0Error{} } -// read_struct reads a single struct of type `T`. +// read_struct reads a single plain-data struct of type `T`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) read_struct[T](mut t T) ! { if !f.is_opened { return error_file_not_opened() @@ -518,7 +519,8 @@ pub fn (mut f File) read_struct[T](mut t T) ! { } } -// read_struct_at reads a single struct of type `T` at position specified in file. +// read_struct_at reads a single plain-data struct of type `T` at position specified in file. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) read_struct_at[T](mut t T, pos u64) ! { if !f.is_opened { return error_file_not_opened() @@ -535,7 +537,8 @@ pub fn (mut f File) read_struct_at[T](mut t T, pos u64) ! { } } -// read_raw reads and returns a single instance of type `T`. +// read_raw reads and returns a single plain-data instance of type `T`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) read_raw[T]() !T { if !f.is_opened { return error_file_not_opened() @@ -552,7 +555,8 @@ pub fn (mut f File) read_raw[T]() !T { return t } -// read_raw_at reads and returns a single instance of type `T` starting at file byte offset `pos`. +// read_raw_at reads and returns a single plain-data instance of type `T` starting at file byte offset `pos`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) read_raw_at[T](pos u64) !T { if !f.is_opened { return error_file_not_opened() @@ -571,7 +575,8 @@ pub fn (mut f File) read_raw_at[T](pos u64) !T { return t } -// write_struct writes a single struct of type `T`. +// write_struct writes a single plain-data struct of type `T`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) write_struct[T](t &T) ! { if !f.is_opened { return error_file_not_opened() @@ -590,7 +595,8 @@ pub fn (mut f File) write_struct[T](t &T) ! { } } -// write_struct_at writes a single struct of type `T` at file byte offset `pos`. +// write_struct_at writes a single plain-data struct of type `T` at file byte offset `pos`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) write_struct_at[T](t &T, pos u64) ! { if !f.is_opened { return error_file_not_opened() @@ -606,7 +612,8 @@ pub fn (mut f File) write_struct_at[T](t &T, pos u64) ! { // TODO: `write_raw[_at]` implementations are copy-pasted from `write_struct[_at]` -// write_raw writes a single instance of type `T`. +// write_raw writes a single plain-data instance of type `T`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) write_raw[T](t &T) ! { if !f.is_opened { return error_file_not_opened() @@ -618,7 +625,8 @@ pub fn (mut f File) write_raw[T](t &T) ! { unsafe { f.write_full_buffer(t, tsize)! } } -// write_raw_at writes a single instance of type `T` starting at file byte offset `pos`. +// write_raw_at writes a single plain-data instance of type `T` starting at file byte offset `pos`. +// `T` must not contain strings, dynamic arrays, maps, pointers, interfaces, or function values. pub fn (mut f File) write_raw_at[T](t &T, pos u64) ! { if !f.is_opened { return error_file_not_opened() diff --git a/vlib/v/checker/check_types.v b/vlib/v/checker/check_types.v index beb306b3a..cae0762b2 100644 --- a/vlib/v/checker/check_types.v +++ b/vlib/v/checker/check_types.v @@ -1422,3 +1422,59 @@ fn (mut c Checker) is_contains_any_kind_of_pointer(typ ast.Type, mut checked_typ } return false } + +fn os_raw_io_unsupported_type_message(path string, type_name string) string { + if path == '' { + return 'contains non-plain-data values of type `${type_name}`' + } + return 'contains field `${path}` of type `${type_name}`' +} + +fn (mut c Checker) os_raw_io_unsupported_type(typ ast.Type, path string, mut checked_types []ast.Type) ?string { + if typ.has_flag(.generic) { + return none + } + clean_typ := typ.clear_flag(.option).clear_flag(.result) + type_name := c.table.type_to_str(typ) + if clean_typ.is_any_kind_of_pointer() + || clean_typ in [ast.voidptr_type, ast.byteptr_type, ast.charptr_type] { + return os_raw_io_unsupported_type_message(path, type_name) + } + base_typ := c.table.unaliased_type(clean_typ) + if base_typ in checked_types { + return none + } + checked_types << base_typ + sym := c.table.final_sym(base_typ) + match sym.kind { + .string, .array, .map, .chan, .function, .interface, .sum_type, .thread, .any, + .multi_return { + return os_raw_io_unsupported_type_message(path, type_name) + } + .array_fixed { + info := sym.info as ast.ArrayFixed + if c.os_raw_io_unsupported_type(info.elem_type, path, mut checked_types) != none { + return os_raw_io_unsupported_type_message(path, type_name) + } + } + .struct { + for field in c.table.struct_fields(sym) { + field_path := if path == '' { field.name } else { '${path}.${field.name}' } + if reason := c.os_raw_io_unsupported_type(field.typ, field_path, mut checked_types) { + return reason + } + } + } + else {} + } + return none +} + +fn (mut c Checker) ensure_os_raw_io_type(typ ast.Type, method_name string, pos token.Pos) { + mut checked_types := []ast.Type{} + if reason := c.os_raw_io_unsupported_type(typ, '', mut checked_types) { + type_name := c.table.type_to_str(typ) + c.error('`os.File.${method_name}` only supports plain data types; `${type_name}` ${reason}', + pos) + } +} diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index af6321ee7..19fc80bb9 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -8,6 +8,26 @@ import os const print_everything_fns = ['println', 'print', 'eprintln', 'eprint', 'panic'] +fn (mut c Checker) check_os_raw_io_call(node &ast.CallExpr, func &ast.Fn, concrete_types []ast.Type, arg_offset int) { + if func.mod != 'os' || !func.is_method { + return + } + match func.name { + 'write_struct', 'write_struct_at', 'write_raw', 'write_raw_at', 'read_struct', + 'read_struct_at' { + if node.args.len > arg_offset { + c.ensure_os_raw_io_type(node.args[arg_offset].typ, func.name, node.args[arg_offset].pos) + } + } + 'read_raw', 'read_raw_at' { + if concrete_types.len > 0 { + c.ensure_os_raw_io_type(concrete_types.last(), func.name, node.pos) + } + } + else {} + } +} + fn (mut c Checker) fn_decl(mut node ast.FnDecl) { // handle vls go to definition for method receiver types if c.pref.is_vls { @@ -2074,6 +2094,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. } } } + c.check_os_raw_io_call(node, func, concrete_types, if func.is_method { 1 } else { 0 }) // resolve return generics struct to concrete type if func.generic_names.len > 0 && func.return_type.has_flag(.generic) @@ -2959,6 +2980,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) if node.concrete_types.len > 0 && method_generic_names_len == 0 { c.error('a non generic function called like a generic one', node.concrete_list_pos) } + c.check_os_raw_io_call(node, method, concrete_types, 0) // resolve return generics struct to concrete type if method_generic_names_len > 0 && method.return_type.has_flag(.generic) && c.table.cur_fn != unsafe { nil } && c.table.cur_fn.generic_names.len == 0 { diff --git a/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.out b/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.out new file mode 100644 index 000000000..b7e6714d1 --- /dev/null +++ b/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.out @@ -0,0 +1,27 @@ +vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv:14:17: error: `os.File.write_struct` only supports plain data types; `Options` contains field `command` of type `string` + 12 | bins: [2, 16] + 13 | } + 14 | f.write_struct(opts) or { panic(err) } + | ~~~~ + 15 | f.write_raw(opts) or { panic(err) } + 16 | mut read_opts := Options{} +vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv:15:14: error: `os.File.write_raw` only supports plain data types; `Options` contains field `command` of type `string` + 13 | } + 14 | f.write_struct(opts) or { panic(err) } + 15 | f.write_raw(opts) or { panic(err) } + | ~~~~ + 16 | mut read_opts := Options{} + 17 | f.read_struct(mut read_opts) or { panic(err) } +vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv:17:20: error: `os.File.read_struct` only supports plain data types; `Options` contains field `command` of type `string` + 15 | f.write_raw(opts) or { panic(err) } + 16 | mut read_opts := Options{} + 17 | f.read_struct(mut read_opts) or { panic(err) } + | ~~~~~~~~~ + 18 | _ = f.read_raw[Options]() or { panic(err) } + 19 | } +vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv:18:8: error: `os.File.read_raw` only supports plain data types; `Options` contains field `command` of type `string` + 16 | mut read_opts := Options{} + 17 | f.read_struct(mut read_opts) or { panic(err) } + 18 | _ = f.read_raw[Options]() or { panic(err) } + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 | } diff --git a/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv b/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv new file mode 100644 index 000000000..297e90c85 --- /dev/null +++ b/vlib/v/checker/tests/os_file_raw_io_unsupported_type_err.vv @@ -0,0 +1,19 @@ +import os + +struct Options { + command string + bins []int +} + +fn main() { + mut f := os.open_file('structwritefile', 'w') or { panic(err) } + opts := Options{ + command: 'explore' + bins: [2, 16] + } + f.write_struct(opts) or { panic(err) } + f.write_raw(opts) or { panic(err) } + mut read_opts := Options{} + f.read_struct(mut read_opts) or { panic(err) } + _ = f.read_raw[Options]() or { panic(err) } +} -- 2.39.5