Skip to content

Commit e019504

Browse files
author
mancausoft
committed
Fix static analysis bugs
1 parent ebd4766 commit e019504

6 files changed

Lines changed: 177 additions & 22 deletions

File tree

BUGS.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# BUGS
2+
3+
Static review as of 2026-05-05. The defects found in this pass have
4+
been fixed in the working tree; this file records what was found and how
5+
it was addressed.
6+
7+
Checks used during the review:
8+
9+
- `make objs`
10+
- `make lib`
11+
- `cc --analyze` on the FUSE-independent wrapper sources
12+
- `git diff --check`
13+
14+
`make bin` was not run locally because this workstation does not expose
15+
FUSE headers through pkg-config.
16+
17+
## Fixed In This Pass
18+
19+
### High: `direct_dirid()` over-copied the `.DIR;1` suffix
20+
21+
Location: `ods2lib/direct.c`
22+
23+
`direct_dirid()` used the total descriptor length (`len + 6`) as the
24+
copy size for the literal `.DIR;1`, reading past the string literal and
25+
potentially writing past `nambuf`.
26+
27+
Fix: copy only the suffix length and validate `len + suffix_len` before
28+
writing into `nambuf`.
29+
30+
### High: long directory specs could abort the FUSE process
31+
32+
Location: `ods2lib/direct.c`, `src/lookup.c`
33+
34+
User-controlled FUSE paths could produce a VMS directory string longer
35+
than `direct_dirid()` accepted, and that path called `abort()`.
36+
37+
Fix: return `SS$_BADFILENAME` instead of aborting, and make POSIX-to-VMS
38+
directory conversion fail on truncation instead of silently shortening
39+
the path.
40+
41+
### High: default multithreaded FUSE mode raced global caches
42+
43+
Location: `src/fuse_ods2.c`
44+
45+
The code recommended `-s`, but did not require it. ods2lib caches and
46+
the textmode decode cache are global mutable state without locking.
47+
48+
Fix: inject `-s` into the libfuse argument vector so the filesystem runs
49+
single-threaded until the cache layers are made thread-safe.
50+
51+
### Medium: long legal names could be truncated in `readdir()`
52+
53+
Location: `src/ops.c`
54+
55+
The display buffer was 80 bytes, but a legal versioned ODS-2 name can be
56+
`40 + "." + 40 + ";" + 5` characters.
57+
58+
Fix: use a buffer sized for the real exposed maximum and report the same
59+
limit through `statfs()`.
60+
61+
### Medium: `readdir()` was not resumable
62+
63+
Location: `src/ops.c`
64+
65+
`readdir()` ignored the incoming offset and passed offset `0` for every
66+
entry, which can break large directories when libfuse needs multiple
67+
calls to drain the listing.
68+
69+
Fix: add stable monotonically increasing offsets for `.`, `..`, and
70+
directory entries; honor the incoming offset; stop cleanly when
71+
`filler()` reports a full buffer.
72+
73+
### Medium: corrupt directory records looked like successful short listings
74+
75+
Location: `src/lookup.c`
76+
77+
Directory parser validation failures broke out of the current block but
78+
still returned `SS$_NORMAL`.
79+
80+
Fix: return `SS$_BADIRECTORY` when record size, record extent, or name
81+
extent checks fail.
82+
83+
### Medium: malformed HEAD timestamps could read outside the header block
84+
85+
Location: `src/lookup.c`
86+
87+
`fh2$b_idoffset` was checked only against the lower fixed-header bound.
88+
A malformed offset near the end of the 512-byte HEAD could make timestamp
89+
reads go out of bounds.
90+
91+
Fix: validate `idoffset * 2 + sizeof(struct IDENT) <= sizeof(struct HEAD)`
92+
before reading the IDENT area.
93+
94+
### Low: `/000000` did not map to the root directory
95+
96+
Location: `src/lookup.c`
97+
98+
The comment promised `/000000` behaved like `/`, but only `/` was special
99+
cased.
100+
101+
Fix: treat both `/` and `/000000` as the MFD.
102+
103+
### Low: smoke test raced first unmount against the second mount
104+
105+
Location: `test/smoke.sh`
106+
107+
The first foreground FUSE process was unmounted but not waited for before
108+
starting the second mount.
109+
110+
Fix: wait for the first background FUSE process after `fusermount3 -u`.

ods2lib/direct.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,8 +1116,8 @@ vmscond_t direct_dirid( struct VCB *vcb, struct dsc$descriptor *dirdsc,
11161116

11171117
dirlen = dirdsc->dsc$w_length;
11181118

1119-
if( (size_t)dirlen > sizeof( nambuf ) -1 )
1120-
abort();
1119+
if( (size_t)dirlen > sizeof( nambuf ) -1 )
1120+
return SS$_BADFILENAME;
11211121
dirnam = dirdsc->dsc$a_pointer;
11221122

11231123
srcdir.fid$w_num = FID$C_MFD;
@@ -1156,9 +1156,12 @@ vmscond_t direct_dirid( struct VCB *vcb, struct dsc$descriptor *dirdsc,
11561156
if( len == 0 )
11571157
break;
11581158

1159-
memcpy( nambuf, bp, len );
1160-
namdsc.dsc$w_length = (uint16_t)(len + sizeof( ".DIR;1" ) -1);
1161-
memcpy( nambuf+len, ".DIR;1", namdsc.dsc$w_length );
1159+
const size_t suffix_len = sizeof( ".DIR;1" ) - 1;
1160+
if( len + suffix_len > sizeof( nambuf ) )
1161+
return SS$_BADFILENAME;
1162+
memcpy( nambuf, bp, len );
1163+
namdsc.dsc$w_length = (uint16_t)(len + suffix_len);
1164+
memcpy( nambuf + len, ".DIR;1", suffix_len );
11621165

11631166
namdsc.dsc$a_pointer = nambuf;
11641167

src/fuse_ods2.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ static void usage( const char *prog ) {
8383
" -o uid=N force a specific uid on every file\n"
8484
" -o gid=N force a specific gid on every file\n"
8585
" -o debug verbose diagnostics on stderr\n"
86-
" -s single-threaded (recommended)\n"
86+
" -s single-threaded (always enabled)\n"
8787
" -f foreground (do not daemonize)\n"
8888
" -d same as -odebug + -f\n"
8989
"\n"
@@ -294,6 +294,10 @@ int main( int argc, char *argv[] ) {
294294
fuse_opt_free_args( &args );
295295
return 0;
296296
}
297+
if( fuse_opt_add_arg( &args, "-s" ) == -1 ) {
298+
fuse_opt_free_args( &args );
299+
return 1;
300+
}
297301

298302
/* Read-only is enforced inside every write op (-EROFS). We do
299303
* NOT inject -oro / -odefault_permissions into the libfuse arg

src/lookup.c

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,10 @@ void ods2_head_to_stat( const struct HEAD *head, struct stat *st ) {
152152
st->st_gid = (gid_t)F11WORD( head->fh2$l_fileowner.uic$w_grp );
153153

154154
/* Times. IDENT area lives at fh2$b_idoffset (units of words). */
155-
if( head->fh2$b_idoffset >= FH2$C_LENGTH / 2 ) {
155+
size_t idoff = (size_t)head->fh2$b_idoffset * sizeof(f11word);
156+
if( idoff >= FH2$C_LENGTH && idoff + sizeof(struct IDENT) <= sizeof *head ) {
156157
const struct IDENT *id = (const struct IDENT *)
157-
((const uint16_t *)head + head->fh2$b_idoffset);
158+
((const char *)head + idoff);
158159
st->st_mtime = ods2_vmstime_to_time_t( id->fi2$q_revdate );
159160
st->st_ctime = ods2_vmstime_to_time_t( id->fi2$q_credate );
160161
st->st_atime = st->st_mtime;
@@ -183,20 +184,24 @@ static char *split_last( char *path ) {
183184
* argument expected by direct_dirid(). Empty input -> empty output
184185
* (which direct_dirid() treats as MFD).
185186
*/
186-
static void posix_dir_to_vms( const char *posix, char *out, size_t outlen ) {
187+
static int posix_dir_to_vms( const char *posix, char *out, size_t outlen ) {
187188
size_t o = 0;
188189
int first = 1;
189-
if( outlen == 0 ) return;
190+
if( outlen == 0 ) return 0;
190191
for( const char *p = posix; *p && o + 1 < outlen; ) {
191192
while( *p == '/' ) ++p;
192193
if( !*p ) break;
193194
if( !first && o + 1 < outlen )
194195
out[o++] = '.';
195196
first = 0;
196-
while( *p && *p != '/' && o + 1 < outlen )
197+
while( *p && *p != '/' ) {
198+
if( o + 1 >= outlen )
199+
return 0;
197200
out[o++] = (char)toupper( (unsigned char)*p++ );
201+
}
198202
}
199203
out[o] = '\0';
204+
return 1;
200205
}
201206

202207
/* Parse "FILE.EXT" or "FILE.EXT;n" into (name, version). When no
@@ -264,6 +269,7 @@ vmscond_t ods2_iterate_dir( struct FCB *dir_fcb, int latest_only,
264269
char last_name[128];
265270
int last_name_len = -1;
266271
int stop = 0;
272+
vmscond_t retsts = SS$_NORMAL;
267273

268274
for( uint32_t blk = 1; blk <= efblk && !stop; ++blk ) {
269275
struct VIOC *vioc = NULL;
@@ -299,23 +305,30 @@ vmscond_t ods2_iterate_dir( struct FCB *dir_fcb, int latest_only,
299305
fprintf( stderr,
300306
"fuse-ods2: iterate_dir: record size=%u too big at offs=%ld\n",
301307
(unsigned)size, (long)(p - buf) );
308+
retsts = SS$_BADIRECTORY;
309+
stop = 1;
302310
break;
303311
}
304312
if( p + size + 2 > end ) {
305313
if( ods2_rt.debug )
306314
fprintf( stderr,
307315
"fuse-ods2: iterate_dir: record overruns block at offs=%ld size=%u\n",
308316
(long)(p - buf), (unsigned)size );
317+
retsts = SS$_BADIRECTORY;
318+
stop = 1;
309319
break;
310320
}
311321

312322
uint8_t namecount = dr->dir$b_namecount;
313323
char *namebase = dr->dir$t_name;
314-
if( namebase + namecount > end ) {
324+
char *rec_end = (char *)dr + size + 2;
325+
if( namebase + namecount > rec_end ) {
315326
if( ods2_rt.debug )
316327
fprintf( stderr,
317328
"fuse-ods2: iterate_dir: name overruns block (count=%u)\n",
318329
(unsigned)namecount );
330+
retsts = SS$_BADIRECTORY;
331+
stop = 1;
319332
break;
320333
}
321334
if( ods2_rt.debug )
@@ -332,7 +345,6 @@ vmscond_t ods2_iterate_dir( struct FCB *dir_fcb, int latest_only,
332345
&& memcmp( last_name, namebase, namecount ) == 0 );
333346

334347
char *eptr = namebase + ((namecount + 1) & ~1);
335-
char *rec_end = (char *)dr + size + 2;
336348
int first = 1;
337349
while( eptr + sizeof(struct dir$r_ent) <= rec_end ) {
338350
struct dir$r_ent *de = (struct dir$r_ent *)eptr;
@@ -363,7 +375,7 @@ vmscond_t ods2_iterate_dir( struct FCB *dir_fcb, int latest_only,
363375
deaccesschunk( vioc, 0, 0, 1 );
364376
}
365377

366-
return SS$_NORMAL;
378+
return retsts;
367379
}
368380

369381
/* ------------------------------------------------------ path resolver */
@@ -412,7 +424,7 @@ vmscond_t ods2_path_to_fid( const char *path, struct fiddef *out_fid,
412424
return SS$_BADPARAM;
413425

414426
/* Root and "/000000" both map to the MFD. */
415-
if( strcmp( path, "/" ) == 0 ) {
427+
if( strcmp( path, "/" ) == 0 || strcmp( path, "/000000" ) == 0 ) {
416428
out_fid->fid$w_num = FID$C_MFD;
417429
out_fid->fid$w_seq = FID$C_MFD;
418430
out_fid->fid$b_rvn = 0;
@@ -439,7 +451,10 @@ vmscond_t ods2_path_to_fid( const char *path, struct fiddef *out_fid,
439451

440452
/* "work" now holds the dir prefix (with leading '/') or empty. */
441453
char vmsdir[512];
442-
posix_dir_to_vms( work, vmsdir, sizeof vmsdir );
454+
if( !posix_dir_to_vms( work, vmsdir, sizeof vmsdir ) ) {
455+
free( work );
456+
return SS$_BADFILENAME;
457+
}
443458

444459
/* Resolve directory part. */
445460
struct dsc_descriptor dirdsc;

src/ops.c

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,13 @@ int ods2_getattr( const char *path, struct stat *st,
101101

102102
/* ------------------------------------------------------ readdir */
103103

104+
#define ODS2_POSIX_NAMEMAX (40 + 1 + 40 + 1 + 5)
105+
104106
struct readdir_ctx {
105107
void *buf;
106108
fuse_fill_dir_t filler;
109+
off_t offset;
110+
off_t next_off;
107111
};
108112

109113
static int has_dir_suffix( const char *raw, int rawlen ) {
@@ -141,9 +145,13 @@ static void emit_name( const char *raw, int rawlen, int version, int is_dir,
141145
static int readdir_cb( const char *name, int namelen, int version,
142146
const struct fiddef *fid, void *ctx ) {
143147
struct readdir_ctx *r = ctx;
144-
char display[80];
148+
char display[ODS2_POSIX_NAMEMAX + 1];
145149
struct stat st;
146150
int is_dir = 0;
151+
off_t this_off = r->next_off++;
152+
153+
if( this_off <= r->offset )
154+
return 0;
147155

148156
struct VIOC *vioc = NULL;
149157
struct HEAD *head = NULL;
@@ -170,7 +178,7 @@ static int readdir_cb( const char *name, int namelen, int version,
170178
*/
171179
st.st_mode = is_dir ? S_IFDIR : S_IFREG;
172180

173-
return r->filler( r->buf, display, &st, 0, 0 );
181+
return r->filler( r->buf, display, &st, this_off, 0 );
174182
}
175183

176184
int ods2_readdir( const char *path, void *buf, fuse_fill_dir_t filler,
@@ -231,10 +239,24 @@ int ods2_readdir( const char *path, void *buf, fuse_fill_dir_t filler,
231239
return -ENOTDIR;
232240
}
233241

234-
filler( buf, ".", NULL, 0, 0 );
235-
filler( buf, "..", NULL, 0, 0 );
242+
off_t next_off = 1;
243+
if( offset < next_off && filler( buf, ".", NULL, next_off, 0 ) != 0 ) {
244+
deaccessfile( dfcb );
245+
return 0;
246+
}
247+
++next_off;
248+
if( offset < next_off && filler( buf, "..", NULL, next_off, 0 ) != 0 ) {
249+
deaccessfile( dfcb );
250+
return 0;
251+
}
252+
++next_off;
236253

237-
struct readdir_ctx ctx = { .buf = buf, .filler = filler };
254+
struct readdir_ctx ctx = {
255+
.buf = buf,
256+
.filler = filler,
257+
.offset = offset,
258+
.next_off = next_off
259+
};
238260

239261
sts = ods2_iterate_dir( dfcb,
240262
ods2_rt.allversions ? 0 : 1,
@@ -404,7 +426,7 @@ int ods2_statfs( const char *path, struct statvfs *st ) {
404426
st->f_bavail = 0;
405427
st->f_files = 0;
406428
st->f_ffree = 0;
407-
st->f_namemax = 80; /* VMS file spec including version */
429+
st->f_namemax = ODS2_POSIX_NAMEMAX;
408430
return 0;
409431
}
410432

test/smoke.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ ok "write attempt is rejected as expected"
7777

7878
# ---- versioned file (HELLO.TXT was copied 3 times -> versions 1..3) ----
7979
fusermount3 -u "$MNT"
80+
wait "$FPID" || true
8081
"$BIN" -s -o allversions "$IMG" "$MNT"
8182
for _ in 1 2 3 4 5; do
8283
if mountpoint -q "$MNT"; then break; fi

0 commit comments

Comments
 (0)