Index: test/test_io.rb
===================================================================
--- test/test_io.rb	(revision 4889)
+++ test/test_io.rb	(working copy)
@@ -216,6 +216,30 @@
     assert_raises(Errno::EBADF) { f.close }
   end
 
+  def test_sysopen
+    ensure_files @file
+
+    fno = IO::sysopen(@file, "r", 0124) # not creating, mode is ignored
+    assert_instance_of(Fixnum, fno)
+    assert_raises(Errno::EINVAL) { IO.open(fno, "w") } # not writable
+    IO.open(fno, "r") do |io|
+      assert_equal(fno, io.fileno)
+      assert(!io.closed?)
+    end
+    assert_raises(Errno::EBADF) { IO.open(fno, "r") } # fd is closed
+    File.open(@file) do |f|
+      mode = (f.stat.mode & 0777) # only comparing lower 9 bits
+      assert(mode > 0124)
+    end
+
+    File.delete(@file)
+    fno = IO::sysopen(@file, "w", 0611) # creating, mode is enforced
+    File.open(@file) do |f|
+      mode = (f.stat.mode & 0777)
+      assert_equal(0611, mode)
+    end
+  end
+
   def test_delete
     ensure_files @file, @file2, @file3
     # Test deleting files
Index: src/org/jruby/Ruby.java
===================================================================
--- src/org/jruby/Ruby.java	(revision 4889)
+++ src/org/jruby/Ruby.java	(working copy)
@@ -126,7 +126,11 @@
 
     public final RubyFixnum[] fixnumCache = new RubyFixnum[256];
     private final RubySymbol.SymbolTable symbolTable = new RubySymbol.SymbolTable(this);
+
     private Hashtable<Integer, WeakReference<IOHandler>> ioHandlers = new Hashtable<Integer, WeakReference<IOHandler>>();
+    // IOHandlers opened by sysopen are cached independently of IO instances
+    private Hashtable<Integer, IOHandler> retainedHandlers = new Hashtable<Integer, IOHandler>();
+
     private long randomSeed = 0;
     private long randomSeedSequence = 0;
     private Random random = new Random();
@@ -2313,6 +2317,10 @@
         return ioHandlers;
     }
 
+    public Hashtable<Integer, IOHandler> getRetainedHandlers() {
+        return retainedHandlers;
+    }
+
     public long incrementRandomSeedSequence() {
         return randomSeedSequence++;
     }
Index: src/org/jruby/RubyFile.java
===================================================================
--- src/org/jruby/RubyFile.java	(revision 4889)
+++ src/org/jruby/RubyFile.java	(working copy)
@@ -56,8 +56,6 @@
 import org.jruby.util.ByteList;
 import org.jruby.util.DirectoryAsFileException;
 import org.jruby.util.IOHandler;
-import org.jruby.util.IOHandlerNull;
-import org.jruby.util.IOHandlerSeekable;
 import org.jruby.util.IOHandlerUnseekable;
 import org.jruby.util.IOModes;
 import org.jruby.util.JRubyFile;
@@ -239,26 +237,9 @@
     }
     
     public void openInternal(String newPath, IOModes newModes) {
+        this.handler = openInternal(getRuntime(), newPath, newModes);
         this.path = newPath;
         this.modes = newModes;
-        
-        try {
-            if (newPath.equals("/dev/null")) {
-                handler = new IOHandlerNull(getRuntime(), newModes);
-            } else {
-                handler = new IOHandlerSeekable(getRuntime(), newPath, newModes);
-            }
-            
-            registerIOHandler(handler);
-        } catch (InvalidValueException e) {
-        	throw getRuntime().newErrnoEINVALError();
-        } catch (DirectoryAsFileException e) {
-            throw getRuntime().newErrnoEISDirError();
-        } catch (FileNotFoundException e) {
-        	throw getRuntime().newErrnoENOENTError();
-        } catch (IOException e) {
-            throw getRuntime().newIOError(e.getMessage());
-		}
     }
     
     @JRubyMethod
@@ -451,17 +432,6 @@
         return "RubyFile(" + path + ", " + modes + ", " + fileno + ")";
     }
 
-    // TODO: This is also defined in the MetaClass too...Consolidate somewhere.
-    private static IOModes getModes(Ruby runtime, IRubyObject object) {
-        if (object instanceof RubyString) {
-            return new IOModes(runtime, ((RubyString) object).toString());
-        } else if (object instanceof RubyFixnum) {
-            return new IOModes(runtime, ((RubyFixnum) object).getLongValue());
-        }
-
-        throw runtime.newTypeError("Invalid type for modes");
-    }
-
     @JRubyMethod
     public IRubyObject inspect() {
         StringBuffer val = new StringBuffer();
Index: src/org/jruby/util/IOHandlerSeekable.java
===================================================================
--- src/org/jruby/util/IOHandlerSeekable.java	(revision 4889)
+++ src/org/jruby/util/IOHandlerSeekable.java	(working copy)
@@ -58,6 +58,7 @@
     protected String cwd;
     protected ByteBuffer buffer; // r/w buffer
     protected boolean reading; // are we reading or writing?
+    protected boolean isNew; // did we create the file?
     protected FileChannel channel;
     
     public IOHandlerSeekable(Ruby runtime, String path, IOModes modes) 
@@ -71,10 +72,11 @@
 
         if (theFile.isDirectory() && modes.isWritable()) throw new DirectoryAsFileException();
         
-        if (modes.isCreate()) theFile.createNewFile();
+        this.isNew = (modes.isCreate() ? theFile.createNewFile() : false);
         
         if (!theFile.exists()) {
             if (modes.isReadable() && !modes.isWritable()) throw new FileNotFoundException();
+            this.isNew = true;
         }
 
         // We always open this rw since we can only open it r or rw.
@@ -102,6 +104,13 @@
         return modes.isWritable() ? "rw" : "r";
     }
 
+    /**
+     * Was a new file created to instantiate this handler?
+     */
+    public boolean isNew() {
+        return isNew;
+    }
+
     public ByteList gets(ByteList separatorString) throws IOException, BadDescriptorException {
         checkReadable();
 
Index: src/org/jruby/util/IOHandlerUnseekable.java
===================================================================
--- src/org/jruby/util/IOHandlerUnseekable.java	(revision 4889)
+++ src/org/jruby/util/IOHandlerUnseekable.java	(working copy)
@@ -124,7 +124,7 @@
         modes = new IOModes(runtime, mode);
         
         if (fileno < 0 || fileno > 2) {
-            throw new IOException("Bad file descriptor");
+            throw runtime.newErrnoEBADFError();
         }
         
         if (modes.isReadable()) {
Index: src/org/jruby/RubyIO.java
===================================================================
--- src/org/jruby/RubyIO.java	(revision 4889)
+++ src/org/jruby/RubyIO.java	(working copy)
@@ -70,7 +70,9 @@
 import org.jruby.util.IOHandlerProcess;
 import org.jruby.util.IOHandlerSeekable;
 import org.jruby.util.IOHandlerUnseekable;
+import org.jruby.util.IOHandler.InvalidValueException;
 import org.jruby.util.IOModes;
+import org.jruby.util.DirectoryAsFileException;
 import org.jruby.util.ShellLauncher;
 
 /**
@@ -153,17 +155,60 @@
      * On reopen, the fileno's IOHandler gets replaced by a new handler. 
      */
     
+    protected static IOHandler openInternal(Ruby runtime, String path, IOModes modes) {
+        return openInternal(runtime, path, modes, false);
+    }
+
+    protected static IOHandler openInternal(Ruby runtime, String path, IOModes modes, boolean isRetained) {
+        IOHandler handler = null;
+        try {
+            if (path.equals("/dev/null")) {
+                handler = new IOHandlerNull(runtime, modes);
+            } else {
+                handler = new IOHandlerSeekable(runtime, path, modes);
+            }
+            registerIOHandler(runtime, handler, isRetained);
+        } catch (InvalidValueException e) {
+            throw runtime.newErrnoEINVALError();
+        } catch (DirectoryAsFileException e) {
+            throw runtime.newErrnoEISDirError();
+        } catch (FileNotFoundException e) {
+            throw runtime.newErrnoENOENTError();
+        } catch (IOException e) {
+            throw runtime.newIOError(e.getMessage());
+        }
+        return handler;
+    }
+
     /*
      * I considered making all callers of this be moved into IOHandlers
      * constructors (since it would be less error prone to forget there).
      * However, reopen() makes doing this a little funky. 
      */
+    protected static void registerIOHandler(Ruby runtime, IOHandler newHandler) {
+        registerIOHandler(runtime, newHandler, false); // not retained
+    }
+
+    protected static void registerIOHandler(Ruby runtime, IOHandler newHandler, boolean isRetained) {
+        Integer cacheKey = new Integer(newHandler.getFileno());
+        runtime.getIoHandlers().put(cacheKey, new WeakReference(newHandler));
+        if (isRetained) {
+            runtime.getRetainedHandlers().put(cacheKey, newHandler);
+        }
+    }
+
+    protected static void unregisterIOHandler(Ruby runtime, int aFileno) {
+        Integer cacheKey = new Integer(aFileno);
+        runtime.getIoHandlers().remove(cacheKey);
+        runtime.getRetainedHandlers().remove(cacheKey);
+    }
+
     public void registerIOHandler(IOHandler newHandler) {
-        getRuntime().getIoHandlers().put(new Integer(newHandler.getFileno()), new WeakReference(newHandler));
+        registerIOHandler(getRuntime(), newHandler);
     }
     
     public void unregisterIOHandler(int aFileno) {
-        getRuntime().getIoHandlers().remove(new Integer(aFileno));
+        unregisterIOHandler(getRuntime(), aFileno);
     }
     
     public IOHandler getIOHandlerByFileno(int aFileno) {
@@ -572,6 +617,35 @@
         return io;
     }
 
+    // path, mode, perms
+
+    @JRubyMethod(name = "sysopen", required = 1, optional = 2, frame = true, meta = true)
+    public static IRubyObject sysopen(IRubyObject recv, IRubyObject[] args, Block block) {
+        Ruby runtime = recv.getRuntime();
+        Arity.checkArgumentCount(runtime, args, 1, 3);
+
+        RubyString pathString = RubyString.stringValue(args[0]);
+        runtime.checkSafeString(pathString);
+        String path = pathString.toString();
+
+        IOModes modes =
+            args.length >= 2 ? getModes(runtime, args[1]) : new IOModes(runtime, IOModes.RDONLY);
+
+        RubyInteger perms =
+            args.length >= 3 ? args[2].convertToInteger() : null;
+
+        IOHandler handler = openInternal(runtime, path, modes, true); // retain
+
+        if (handler instanceof IOHandlerSeekable) {
+            if (((IOHandlerSeekable)handler).isNew() && perms != null) {
+                Ruby.getPosix().chmod(path, (int) perms.getLongValue());
+            }
+        }
+
+        int fileno = handler.getFileno();
+        return runtime.newFixnum(fileno);
+    }
+
     // This appears to be some windows-only mode.  On a java platform this is a no-op
     @JRubyMethod(name = "binmode")
     public IRubyObject binmode() {
@@ -1587,6 +1661,16 @@
        });
    }
    
+    // TODO: This is also defined in the MetaClass too...Consolidate somewhere.
+    protected static IOModes getModes(Ruby runtime, IRubyObject object) {
+        if (object instanceof RubyString) {
+            return new IOModes(runtime, ((RubyString) object).toString());
+        } else if (object instanceof RubyFixnum) {
+            return new IOModes(runtime, ((RubyFixnum) object).getLongValue());
+        }
+        throw runtime.newTypeError("Invalid type for modes");
+    }
+
     /**
      * returns non-nil if input available without blocking, false if EOF or not open/readable, otherwise nil.
      */

