Skip to content

Commit 2b56641

Browse files
branchseerclaude
andcommitted
fix(fspy): handle null ObjectName and Buffer in UNICODE_STRING on Windows
When oxlint runs with --type-aware flag, it spawns tsgolint which triggers code paths where ObjectName or its Buffer can be null. This caused a panic in slice::from_raw_parts due to null pointer. Fixes: - Check for null ObjectName before dereferencing in POBJECT_ATTRIBUTES - Handle null/empty Buffer in get_u16_str by returning empty slice - Fix UNICODE_STRING.Length interpretation (bytes, not char count) Also adds oxlint_type_aware test to verify the fix and prevent regression. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent db0eaf1 commit 2b56641

File tree

5 files changed

+77
-10
lines changed

5 files changed

+77
-10
lines changed

crates/fspy/tests/oxlint.rs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
mod test_utils;
22

3-
use std::{env::vars_os, process::Stdio};
3+
use std::{env::vars_os, ffi::OsString};
44

55
use fspy::{AccessMode, PathAccessIterable};
66
use test_log::test;
77

8-
/// Find the oxlint executable in test_bins
9-
fn find_oxlint() -> std::path::PathBuf {
10-
let test_bins_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
8+
/// Get the test_bins/.bin directory path
9+
fn test_bins_bin_dir() -> std::path::PathBuf {
10+
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1111
.parent()
1212
.unwrap()
1313
.join("vite_task_bin")
1414
.join("test_bins")
1515
.join("node_modules")
16-
.join(".bin");
16+
.join(".bin")
17+
}
1718

19+
/// Find the oxlint executable in test_bins
20+
fn find_oxlint() -> std::path::PathBuf {
21+
let test_bins_dir = test_bins_bin_dir();
1822
which::which_in("oxlint", Some(&test_bins_dir), std::env::current_dir().unwrap())
1923
.expect("oxlint not found in test_bins/node_modules/.bin")
2024
}
2125

2226
async fn track_oxlint(dir: &std::path::Path, args: &[&str]) -> anyhow::Result<PathAccessIterable> {
2327
let oxlint_path = find_oxlint();
2428
let mut command = fspy::Command::new(&oxlint_path);
25-
command.args(args).stdout(Stdio::null()).stderr(Stdio::null()).envs(vars_os()).current_dir(dir);
29+
30+
// Build PATH with test_bins/.bin prepended so oxlint can find tsgolint
31+
let test_bins_dir = test_bins_bin_dir();
32+
let new_path = if let Some(existing_path) = std::env::var_os("PATH") {
33+
let mut paths = vec![test_bins_dir.as_os_str().to_owned()];
34+
paths.extend(std::env::split_paths(&existing_path).map(|p| p.into_os_string()));
35+
std::env::join_paths(paths)?
36+
} else {
37+
OsString::from(&test_bins_dir)
38+
};
39+
40+
command
41+
.args(args)
42+
.envs(vars_os().filter(|(k, _)| !k.eq_ignore_ascii_case("PATH")))
43+
.env("PATH", new_path)
44+
.current_dir(dir);
2645

2746
let child = command.spawn().await?;
2847
let termination = child.wait_handle.await?;
@@ -64,3 +83,38 @@ async fn oxlint_reads_directory() -> anyhow::Result<()> {
6483
test_utils::assert_contains(&accesses, &tmpdir_path, AccessMode::READ_DIR);
6584
Ok(())
6685
}
86+
87+
/// Test oxlint with TypeScript type-aware linting (--tsconfig)
88+
/// This reproduces a crash in fspy_preload_windows on Windows:
89+
/// "unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be aligned and non-null"
90+
#[test(tokio::test)]
91+
async fn oxlint_type_aware() -> anyhow::Result<()> {
92+
let tmpdir = tempfile::tempdir()?;
93+
94+
// Create a simple TypeScript file
95+
let ts_file = tmpdir.path().join("index.ts");
96+
std::fs::write(
97+
&ts_file,
98+
r#"
99+
import type { Foo } from './types';
100+
declare const _foo: Foo;
101+
"#,
102+
)?;
103+
104+
// Run oxlint without --type-aware first
105+
let accesses = track_oxlint(tmpdir.path(), &[""]).await?;
106+
let access_to_types_ts = accesses.iter().find(|access| {
107+
let os_str = access.path.to_cow_os_str();
108+
os_str.as_encoded_bytes().ends_with(b"\\types.ts")
109+
|| os_str.as_encoded_bytes().ends_with(b"/types.ts")
110+
});
111+
assert_eq!(access_to_types_ts, None, "oxlint should not read types.ts without --type-aware");
112+
113+
// Run oxlint with --type-aware to enable type-aware linting
114+
let accesses = track_oxlint(tmpdir.path(), &["--type-aware"]).await?;
115+
116+
// Check that oxlint read types.ts
117+
test_utils::assert_contains(&accesses, &tmpdir.path().join("types.ts"), AccessMode::READ);
118+
119+
Ok(())
120+
}

crates/fspy_preload_windows/src/windows/convert.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ impl ToAbsolutePath for POBJECT_ATTRIBUTES {
5050
self,
5151
f: F,
5252
) -> winsafe::SysResult<R> {
53-
let filename_str = unsafe { get_u16_str(&*(*self).ObjectName) };
53+
let filename_str = if let Some(object_name) = unsafe { (*self).ObjectName.as_ref() } {
54+
unsafe { get_u16_str(object_name) }
55+
} else {
56+
U16Str::from_slice(&[])
57+
};
5458
let filename_slice = filename_str.as_slice();
5559
let is_absolute = (filename_slice.get(0) == Some(&b'\\'.into())
5660
&& filename_slice.get(1) == Some(&b'\\'.into())) // \\...

crates/fspy_preload_windows/src/windows/winapi_utils.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ pub fn ck_long(val: c_long) -> winsafe::SysResult<()> {
2929
}
3030

3131
pub unsafe fn get_u16_str(ustring: &UNICODE_STRING) -> &U16Str {
32-
let chars =
33-
unsafe { slice::from_raw_parts((*ustring).Buffer, (*ustring).Length.try_into().unwrap()) };
32+
// https://learn.microsoft.com/en-us/windows/win32/api/subauth/ns-subauth-unicode_string
33+
// UNICODE_STRING.Length is in bytes
34+
let u16_count = ustring.Length / 2;
35+
let chars: &[u16] = if u16_count == 0 {
36+
// If length is zero, we cann't use slice::from_raw_parts as it requires a non-null pointer but
37+
// Buffer may be null in that case.
38+
&[]
39+
} else {
40+
unsafe { slice::from_raw_parts((*ustring).Buffer, u16_count.try_into().unwrap()) }
41+
};
3442
match U16CStr::from_slice_truncate(chars) {
3543
Ok(ok) => ok.as_ustr(),
3644
Err(_) => chars.into(),

crates/fspy_shared/src/ipc/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl Debug for AccessMode {
3232
}
3333
}
3434

35-
#[derive(Encode, BorrowDecode, Debug, Clone, Copy)]
35+
#[derive(Encode, BorrowDecode, Debug, Clone, Copy, PartialEq, Eq)]
3636
pub struct PathAccess<'a> {
3737
pub mode: AccessMode,
3838
pub path: &'a NativeStr,

crates/vite_task_bin/test_bins/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@yarnpkg/shell": "catalog:",
1313
"cross-env": "^10.1.0",
1414
"oxlint": "catalog:",
15+
"oxlint-tsgolint": "catalog:",
1516
"vite-task-test-bins": "link:"
1617
}
1718
}

0 commit comments

Comments
 (0)