@@ -204,6 +204,82 @@ describe("sqlite WAL maintenance", () => {
204204 }
205205 } ) ;
206206
207+ it ( "refuses fuse.sshfs mountinfo entries" , ( ) => {
208+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-sshfs-" ) ) ;
209+ try {
210+ const db = createMockDb ( ) ;
211+ vi . spyOn ( fs , "statfsSync" ) . mockReturnValue ( statfsFixture ( 0 ) ) ;
212+ vi . spyOn ( fs , "readFileSync" ) . mockReturnValue (
213+ `42 12 0:41 / ${ tempDir } rw,relatime - fuse.sshfs user@host:/share rw\n` ,
214+ ) ;
215+
216+ expect ( ( ) =>
217+ configureSqliteWalMaintenance ( db , {
218+ checkpointIntervalMs : 0 ,
219+ databaseLabel : "test-db" ,
220+ databasePath : path . join ( tempDir , "openclaw.sqlite" ) ,
221+ } ) ,
222+ ) . toThrow ( / t e s t - d b .* S S H F S .* r e f u s i n g t o o p e n / ) ;
223+
224+ expect ( db [ "prepare" ] ) . not . toHaveBeenCalled ( ) ;
225+ expect ( db [ "exec" ] ) . not . toHaveBeenCalled ( ) ;
226+ } finally {
227+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
228+ }
229+ } ) ;
230+
231+ it ( "refuses symlinked paths into fuse.sshfs mounts" , ( ) => {
232+ if ( process . platform === "win32" ) {
233+ return ;
234+ }
235+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-sshfs-link-" ) ) ;
236+ const mountDir = path . join ( tempDir , "mount" ) ;
237+ const linkedDir = path . join ( tempDir , "linked" ) ;
238+ try {
239+ fs . mkdirSync ( mountDir ) ;
240+ fs . symlinkSync ( mountDir , linkedDir ) ;
241+ vi . spyOn ( fs , "statfsSync" ) . mockReturnValue ( statfsFixture ( 0 ) ) ;
242+ vi . spyOn ( fs , "readFileSync" ) . mockReturnValue (
243+ `42 12 0:41 / ${ mountDir } rw,relatime - fuse.sshfs user@host:/share rw\n` ,
244+ ) ;
245+
246+ expect ( ( ) =>
247+ configureSqliteWalMaintenance ( createMockDb ( ) , {
248+ checkpointIntervalMs : 0 ,
249+ databasePath : path . join ( linkedDir , "openclaw.sqlite" ) ,
250+ } ) ,
251+ ) . toThrow ( / S S H F S .* r e f u s i n g t o o p e n / ) ;
252+ } finally {
253+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
254+ }
255+ } ) ;
256+
257+ it ( "matches raw mount paths when the existing path canonicalizes elsewhere" , ( ) => {
258+ if ( process . platform === "win32" ) {
259+ return ;
260+ }
261+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-sshfs-prefix-" ) ) ;
262+ const canonicalMountDir = path . join ( tempDir , "canonical-mount" ) ;
263+ const rawMountDir = path . join ( tempDir , "raw-mount" ) ;
264+ try {
265+ fs . mkdirSync ( canonicalMountDir ) ;
266+ fs . symlinkSync ( canonicalMountDir , rawMountDir ) ;
267+ vi . spyOn ( fs , "statfsSync" ) . mockReturnValue ( statfsFixture ( 0 ) ) ;
268+ vi . spyOn ( fs , "readFileSync" ) . mockReturnValue (
269+ `42 12 0:41 / ${ rawMountDir } rw,relatime - fuse.sshfs user@host:/share rw\n` ,
270+ ) ;
271+
272+ expect ( ( ) =>
273+ configureSqliteWalMaintenance ( createMockDb ( ) , {
274+ checkpointIntervalMs : 0 ,
275+ databasePath : path . join ( rawMountDir , "openclaw.sqlite" ) ,
276+ } ) ,
277+ ) . toThrow ( / S S H F S .* r e f u s i n g t o o p e n / ) ;
278+ } finally {
279+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
280+ }
281+ } ) ;
282+
207283 it ( "uses mount command filesystem names on platforms without proc mountinfo" , ( ) => {
208284 const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-nfs-" ) ) ;
209285 try {
@@ -250,6 +326,60 @@ describe("sqlite WAL maintenance", () => {
250326 }
251327 } ) ;
252328
329+ it . each ( [
330+ [ "macfuse" , "sshfs#user@host:/share" ] ,
331+ [ "macfuse" , "host:/share" ] ,
332+ [ "macfuse" , "user@host:" ] ,
333+ [ "osxfuse" , "user@host:/share" ] ,
334+ [ "osxfuse" , "sshfs@osxfuse0" ] ,
335+ ] ) ( "refuses SSHFS reported as %s by mount" , ( fsType , source ) => {
336+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-sshfs-macfuse-" ) ) ;
337+ try {
338+ const db = createMockDb ( ) ;
339+ vi . spyOn ( fs , "statfsSync" ) . mockReturnValue ( statfsFixture ( 0 ) ) ;
340+ vi . spyOn ( fs , "readFileSync" ) . mockImplementation ( ( ) => {
341+ throw new Error ( "no proc mountinfo" ) ;
342+ } ) ;
343+ vi . spyOn ( childProcess , "execFileSync" ) . mockReturnValue (
344+ Buffer . from ( `${ source } on ${ tempDir } (${ fsType } , nodev, nosuid)\n` ) ,
345+ ) ;
346+
347+ expect ( ( ) =>
348+ configureSqliteWalMaintenance ( db , {
349+ checkpointIntervalMs : 0 ,
350+ databasePath : path . join ( tempDir , "openclaw.sqlite" ) ,
351+ } ) ,
352+ ) . toThrow ( / r e f u s i n g t o o p e n / ) ;
353+
354+ expect ( db [ "exec" ] ) . not . toHaveBeenCalled ( ) ;
355+ } finally {
356+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
357+ }
358+ } ) ;
359+
360+ it ( "keeps WAL enabled for non-remote macFUSE mounts" , ( ) => {
361+ const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-macfuse-" ) ) ;
362+ try {
363+ const db = createMockDb ( ) ;
364+ vi . spyOn ( fs , "statfsSync" ) . mockReturnValue ( statfsFixture ( 0 ) ) ;
365+ vi . spyOn ( fs , "readFileSync" ) . mockImplementation ( ( ) => {
366+ throw new Error ( "no proc mountinfo" ) ;
367+ } ) ;
368+ vi . spyOn ( childProcess , "execFileSync" ) . mockReturnValue (
369+ Buffer . from ( `remote-volume on ${ tempDir } (macfuse, nodev, nosuid)\n` ) ,
370+ ) ;
371+
372+ configureSqliteWalMaintenance ( db , {
373+ checkpointIntervalMs : 0 ,
374+ databasePath : path . join ( tempDir , "openclaw.sqlite" ) ,
375+ } ) ;
376+
377+ expect ( db [ "exec" ] ) . toHaveBeenNthCalledWith ( 1 , "PRAGMA journal_mode = WAL;" ) ;
378+ } finally {
379+ fs . rmSync ( tempDir , { recursive : true , force : true } ) ;
380+ }
381+ } ) ;
382+
253383 it ( "parses Linux mount command filesystem names when proc mountinfo is unavailable" , ( ) => {
254384 const tempDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "openclaw-sqlite-nfs-" ) ) ;
255385 try {
0 commit comments