Repository: angr/angr

Issue:
Unlink simprocedure missing errno behavior
<!--
*Disclaimer:
The angr suite is maintained by a small team of volunteers.
While we cannot guarantee any timeliness for fixes and enhancements, we will do our best.
For more real-time help with angr, from us and the community, join our [Slack.](http://angr.io/invite/)*
-->

---

**Is your feature request related to a problem? Please describe.**
The unlink() function/syscall can set a diverse range of errno values. The simprocedure version of unlink doesn't model any of them, which causes incorrect execution of programs that use this interface.

The problem I'm specifically having is distinguishing failure because an existing file can't be removed, and failure because there is no file to remove.

**Describe the solution you would like.**
Add relevant errno assignments to the simprocedure.  I'm not sure if the simfs function gives enough data to decide which error to return on failure, so its return might need to be augmented, as well.

**Please include a sample of what *should* work if this feature is implemented.**
Currently, the marked branch in the following C code is unreachable:

```
int my_remove() {
   if(remove("/foo/bar")) {
      if(ERRNO == ENOENT) {
         //UNREACHABLE D:
         return 0
      }
      return 1
   }
   return 0
}
```

**Describe alternatives you have considered.**
Forcing the simfs remove() operation to succeed would make my problem go away, but it doesn't solve the general issue that code reading the errno values after a failure won't work properly.

**Additional context.**
<!--
Add any other context or screenshots about the feature request here.
-->



Discussion:
Sounds not super hard. If there's not enough information returned from just the call to remove, the simprocedure can use the simfs apis to probe for what the situation was - there are no race conditions in angr :P

For guidance on how to use errno in simprocedures, look at angr/procedures/posix/dup.py. The use of `state.libc.ret_errno` correctly sets the global errno or the return value depending on if the procedure is being used as a syscall or a library function.
@rhelmot I will work on this over the next few days and let you know if I have any questions.

Resolving patch:
diff --git a/angr/procedures/linux_kernel/unlink.py b/angr/procedures/linux_kernel/unlink.py
--- a/angr/procedures/linux_kernel/unlink.py
+++ b/angr/procedures/linux_kernel/unlink.py
@@ -15,6 +15,10 @@ def run(self, path_addr):
         str_expr = self.state.memory.load(path_addr, p_strlen.max_null_index, endness='Iend_BE')
         str_val = self.state.solver.eval(str_expr, cast_to=bytes)
 
+        # Check if entity exists before attempting to unlink
+        if not self.state.fs.get(str_val):
+            return self.state.libc.ret_errno('ENOENT')
+
         if self.state.fs.delete(str_val):
             return 0
         else:


Test patch:
diff --git a/tests/test_unlink.py b/tests/test_unlink.py
new file mode 100644
--- /dev/null
+++ b/tests/test_unlink.py
@@ -0,0 +1,36 @@
+import angr
+import nose
+
+def test_file_unlink():
+    # Initialize a blank state with an arbitrary errno location
+    state = angr.SimState(arch="AMD64", mode="symbolic")
+    state.libc.errno_location = 0xa0000000
+    state.libc.errno = 0
+
+    # Create a file 'test'
+    fd = state.posix.open(b'test', 1)
+    state.posix.close(fd)
+
+    # Ensure 'test' was in fact created
+    nose.tools.assert_in(b'/test', state.fs._files)
+
+    # Store the filename in memory
+    path_addr = 0xb0000000
+    state.memory.store(path_addr, b'test\x00')
+
+    # Unlink 'test': should return 0 and leave ERRNO unchanged
+    unlink = angr.SIM_PROCEDURES['posix']['unlink']()
+    state.scratch.sim_procedure = unlink
+    rval = unlink.execute(state, arguments=[path_addr]).ret_expr
+    nose.tools.assert_equal(rval, 0)
+    nose.tools.assert_equal(state.solver.eval(state.libc.errno), 0)
+
+    # Check that 'test' was in fact deleted
+    nose.tools.assert_equal(state.fs._files, {})
+
+    # Unlink again: should return -1 and set ERRNO to ENOENT
+    unlink = angr.SIM_PROCEDURES['posix']['unlink']()
+    state.scratch.sim_procedure = unlink
+    rval = unlink.execute(state, arguments=[path_addr]).ret_expr
+    nose.tools.assert_equal(rval, -1)
+    nose.tools.assert_equal(state.solver.eval(state.libc.errno), state.posix.ENOENT)
\ No newline at end of file